Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55e5e0153f | |||
| e7889f80a3 | |||
| a159eb2b8b | |||
| 28ee44bcf1 |
@@ -24,6 +24,11 @@ API_SERVER_URL=http://localhost:8000
|
|||||||
# Each physical tablet/PC deployment must set this unique per-station value.
|
# Each physical tablet/PC deployment must set this unique per-station value.
|
||||||
# Leave empty only for a single-station all-in-one demo using ST-DEFAULT.
|
# Leave empty only for a single-station all-in-one demo using ST-DEFAULT.
|
||||||
STATION_CODE=ST-DEFAULT
|
STATION_CODE=ST-DEFAULT
|
||||||
|
# Allow switching station from the URL (?station=CODE) during commissioning, so a
|
||||||
|
# single PC can exercise several stations. Leave at 0 in production: on the shop
|
||||||
|
# floor the station identity comes from the local install, and measuring against
|
||||||
|
# another station's recipes would silently break traceability.
|
||||||
|
STATION_SWITCH_ENABLED=0
|
||||||
|
|
||||||
# --- File Storage ---
|
# --- File Storage ---
|
||||||
# Resolved against the project root in src/backend/config.py.
|
# Resolved against the project root in src/backend/config.py.
|
||||||
|
|||||||
@@ -0,0 +1,614 @@
|
|||||||
|
# TieMeasureFlow — modifiche da apportare
|
||||||
|
|
||||||
|
**Data:** 28 luglio 2026 (agg. serale: architettura d'installazione, punti 7-8 precisati, punti 14-15 nuovi, timer bidirezionale nel punto 3)
|
||||||
|
**Riferimento codice:** branch `V2.0.0`, commit `2a56632`
|
||||||
|
**Destinatari:** Parte 1 leggibile da cliente e rivenditore · Parte 2 per chi mette mano al codice
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Come leggere questo documento
|
||||||
|
|
||||||
|
La **Parte 1** dice *cosa deve fare* il sistema e *perché*: si può mandare a Ricerca e
|
||||||
|
Misure e a Tràfilo. La **Parte 2** dice *dove si interviene*, con file e criteri di
|
||||||
|
completamento. La **Parte 3** elenca le domande la cui risposta non dipende da noi.
|
||||||
|
|
||||||
|
Ogni punto porta lo **stato verificato sul codice**, non riferito: `già fatto`,
|
||||||
|
`parziale`, `da fare`. Serve a evitare l'equivoco emerso in questi giorni — diverse
|
||||||
|
funzioni risultano "già implementate" perché **l'interfaccia c'è**, ma dietro non
|
||||||
|
succede nulla.
|
||||||
|
|
||||||
|
**Fonti:** specifica *Schema sviluppo SW TieFlow rev. 04-2026*; *MODIFICHE TIEMEASURE
|
||||||
|
v260519*; call del 28/07/2026; rettifica di Marco Menoncin del 28/07; lettura diretta
|
||||||
|
del codice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Il punto che regge tutti gli altri
|
||||||
|
|
||||||
|
**Lo stato della produzione oggi vive dentro una pagina del browser.**
|
||||||
|
|
||||||
|
Il timer, il conteggio dei cicli, il flag "produzione avviata" e le misure in corso
|
||||||
|
sono variabili del componente Alpine di `task_execute.html`. La navigazione fra un
|
||||||
|
task e l'altro è un **ricaricamento completo di pagina** (`window.location.href`).
|
||||||
|
|
||||||
|
Conseguenza diretta: **cambiando task si perde tutto.** Il timer smette di esistere,
|
||||||
|
il conteggio dei cicli riparte, "produzione avviata" si dimentica.
|
||||||
|
|
||||||
|
Questo spiega, senza bisogno di altre ipotesi:
|
||||||
|
|
||||||
|
- perché il loop di misura non regge quando la ricetta ha altri task dopo la misura;
|
||||||
|
- perché con più task di misura non si sa a quale tornare allo scadere del timer;
|
||||||
|
- perché *fermo linea* e *fine produzione* non hanno nulla da fermare o da chiudere;
|
||||||
|
- perché non esiste storico di cosa è successo durante una produzione.
|
||||||
|
|
||||||
|
**Va risolto per primo**: i punti 2, 3, 6 e 8 dipendono da questa scelta, e affrontarli
|
||||||
|
prima significa rifarli dopo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architettura d'installazione (decisa il 28/07)
|
||||||
|
|
||||||
|
Chiude la domanda D-3 lato Tielogic. L'installazione a Tràfilo è così composta:
|
||||||
|
|
||||||
|
```
|
||||||
|
SERVER (uno, del cliente) STAZIONE (una per PC, installata in locale)
|
||||||
|
┌─────────────────────────┐ ┌──────────────────────────────────────┐
|
||||||
|
│ MySQL │◄──API──│ App di stazione: │
|
||||||
|
│ Backend dati (API) │ │ · interfaccia operatore (frontend) │
|
||||||
|
│ File dei disegni │ │ · agente hardware: calibro USB, │
|
||||||
|
│ Migrazioni dello schema │ │ driver camera, colonnina/cicalino │
|
||||||
|
│ Adattatore GAIA │ │ · elaborazione visione (futura: │
|
||||||
|
└─────────────────────────┘ │ XF compare, pattern matching) │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Sul server vive tutto ciò che è stato**: il database, i file dei disegni, le
|
||||||
|
migrazioni dello schema e l'unico punto di contatto col gestionale GAIA.
|
||||||
|
Aggiornare la logica dati = un deploy, in un posto solo.
|
||||||
|
- **Sulla stazione vive tutto ciò che tocca l'hardware**: calibro, telecamera,
|
||||||
|
segnalazione luminosa/acustica, e in prospettiva l'elaborazione visione — che
|
||||||
|
così non carica il server (rilevante per il dimensionamento, D-4).
|
||||||
|
L'app di stazione è **senza stato**: non tocca il database direttamente, parla
|
||||||
|
col backend dati via API e gli manda risultati e immagini.
|
||||||
|
- **L'identità della stazione è data dall'installazione** (configurata sul PC),
|
||||||
|
non più dal container sul server: il vincolo "una stazione = un container"
|
||||||
|
decade.
|
||||||
|
- La **licenza per postazione** ha l'aggancio naturale nell'app di stazione
|
||||||
|
installata.
|
||||||
|
|
||||||
|
**Correzione a quanto detto in call:** i disegni **non stanno nel database** —
|
||||||
|
stanno su filesystem (`uploads/`), serviti dal backend. Con questa architettura
|
||||||
|
restano sul server, in un posto solo: nessuna cartella da sincronizzare fra i PC.
|
||||||
|
|
||||||
|
Conseguenza per lo sviluppo: lo stato della produzione (punto 1) va nel database
|
||||||
|
**per obbligo architetturale**, non per scelta — con 20 app di stazione non esiste
|
||||||
|
altro posto dove possa vivere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Parte 1 — Cosa deve fare il sistema
|
||||||
|
|
||||||
|
## Quadro: cosa già funziona
|
||||||
|
|
||||||
|
Da mettere a verbale, perché in parte era dato per mancante:
|
||||||
|
|
||||||
|
| Funzione | Stato |
|
||||||
|
|---|---|
|
||||||
|
| Stazioni e assegnazione ricette alle stazioni | ✅ presente |
|
||||||
|
| Ruolo **capoturno** con autorizzazione a login | ✅ presente |
|
||||||
|
| Timer d'intervallo misura con **conto alla rovescia a video** e cicalino | ✅ presente |
|
||||||
|
| Logout automatico per inattività, configurabile | ✅ presente |
|
||||||
|
| Tolleranze con soglie di attenzione (UTL/UWL/LWL/LTL) ed esito per quota | ✅ presente |
|
||||||
|
| Versionamento delle ricette con storico delle modifiche | ✅ presente |
|
||||||
|
| Registrazione del **tempo di inserimento** di ogni misura | ✅ presente (28/07) |
|
||||||
|
| Pulsanti *Fermo linea*, *Fine produzione*, *Avvio produzione* | ⚠️ solo interfaccia |
|
||||||
|
|
||||||
|
L'ultima riga è la più importante: i tre pulsanti esistono, chiedono correttamente
|
||||||
|
l'autorizzazione del capoturno, **e poi non fanno nulla**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Memoria della produzione in corso `da fare` · prerequisito
|
||||||
|
|
||||||
|
**Oggi:** lo stato di una produzione esiste solo finché l'operatore resta sulla stessa
|
||||||
|
schermata.
|
||||||
|
|
||||||
|
**Deve:** la produzione diventa un'entità con una vita propria — si apre quando parte,
|
||||||
|
registra ciò che accade (avvio, cicli di misura, fermo linea, ripresa, chiusura) e si
|
||||||
|
chiude quando il capoturno la chiude. Se l'operatore cambia task, esce e rientra, o il
|
||||||
|
tablet si riavvia, la produzione è ancora lì con il suo timer.
|
||||||
|
|
||||||
|
**Perché conta:** senza questo, *fermo linea* e *fine produzione* non possono
|
||||||
|
funzionare, il loop di misura non è realizzabile e non esiste il dato storico su cui
|
||||||
|
poggiano sia la statistica sia l'integrazione col gestionale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tipo di task esplicito `da fare`
|
||||||
|
|
||||||
|
**Oggi:** il sistema distingue un task di misura da un task documentale **deducendolo**:
|
||||||
|
se ha delle quote è una misura, altrimenti è una nota. Non esiste un tipo dichiarato.
|
||||||
|
|
||||||
|
**Deve:** ogni task nasce con un tipo scelto da chi crea la ricetta — **nota**,
|
||||||
|
**misura**, **disegno** — predisposto per i tipi futuri (confronto profilo/DXF, misura
|
||||||
|
con camera).
|
||||||
|
|
||||||
|
**Perché conta:** è la richiesta n. 1 di Menoncin, ed è ciò a cui si agganciano timer e
|
||||||
|
loop. Con la deduzione attuale un task di misura a cui non sono ancora state inserite le
|
||||||
|
quote viene trattato come una nota: il sistema si comporta in modo diverso a seconda di
|
||||||
|
quanto è completa la ricetta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Loop di misura e ripetizione `parziale`
|
||||||
|
|
||||||
|
**Oggi:** allo scadere del timer suona il cicalino e il ciclo si riazzera — ma solo se
|
||||||
|
l'operatore è rimasto su quella schermata.
|
||||||
|
|
||||||
|
**Deve:**
|
||||||
|
- l'operatore esegue i task in sequenza fino al primo task di misura;
|
||||||
|
- da lì **resta in loop sulla misura** finché la produzione non viene chiusa;
|
||||||
|
- il conto alla rovescia di **quanto manca alla prossima misura** è sempre visibile;
|
||||||
|
- **arrivato a zero, il contatore riparte nell'altro senso**: mostra da quanto tempo
|
||||||
|
si è **oltre** l'intervallo di misura, in evidenza — così il ritardo si vede, non
|
||||||
|
si deduce (richiesta dell'operatore, 28/07);
|
||||||
|
- allo scadere del timer la misura **si ripropone** ovunque si trovi l'operatore;
|
||||||
|
- se la ricetta ha **più task di misura**, il timer riparte alla fine dell'**ultimo**;
|
||||||
|
- dev'essere possibile **girare il pezzo e rimisurare** senza chiudere il ciclo.
|
||||||
|
|
||||||
|
**Perché conta:** è la seconda richiesta di Menoncin e il comportamento descritto al
|
||||||
|
punto 4.10 della specifica del 19/05. La logica c'è già in gran parte: manca che
|
||||||
|
sopravviva al cambio di schermata (punto 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Limite di tentativi prima del capoturno `da fare`
|
||||||
|
|
||||||
|
**Oggi:** l'operatore può ripetere la misura quante volte vuole.
|
||||||
|
|
||||||
|
**Deve:** dopo un numero di tentativi definito nella ricetta, per proseguire serve
|
||||||
|
l'autorizzazione del capoturno.
|
||||||
|
|
||||||
|
**Perché conta:** è nella specifica ed è la contromisura al caso in cui si ripete finché
|
||||||
|
non "viene bene". Il meccanismo di autorizzazione esiste già: manca il contatore e la
|
||||||
|
soglia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Avanzamento solo se in tolleranza `da verificare`
|
||||||
|
|
||||||
|
**Deve:** si passa alla quota successiva in autonomia **solo se la quota è in
|
||||||
|
tolleranza**; per confermare una quota fuori tolleranza serve il capoturno.
|
||||||
|
|
||||||
|
**Nota:** il gate del capoturno per il fuori tolleranza è implementato. Va verificato
|
||||||
|
sul campo che **blocchi davvero l'avanzamento** e non sia solo una richiesta di conferma.
|
||||||
|
Se blocca, il punto si chiude senza sviluppo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Fermo linea e Fine produzione: dare effetto `parziale`
|
||||||
|
|
||||||
|
**Oggi:** entrambi chiedono l'autorizzazione del capoturno, poi non succede niente.
|
||||||
|
|
||||||
|
**Deve:**
|
||||||
|
- **Fermo linea** — sospende il timer e la produzione; il capoturno può riattivarla;
|
||||||
|
- **Fine produzione** — chiude la produzione, ferma il timer definitivamente e **invia
|
||||||
|
i dati di misura dell'intera produzione** al file di statistica.
|
||||||
|
|
||||||
|
**Perché conta:** il comportamento verso il gestionale è ancora da definire (Parte 3),
|
||||||
|
ma **tutto ciò che sta prima del gestionale si può e si deve fare adesso**: sospendere,
|
||||||
|
riprendere, chiudere, registrare. Consegnare i pulsanti funzionanti senza il gestionale
|
||||||
|
è possibile; il contrario no.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Gestione delle stazioni `parziale`
|
||||||
|
|
||||||
|
**Deve:**
|
||||||
|
- la lista stazioni mostra, oltre a codice e postazione, **le ricette collegate**;
|
||||||
|
- esiste un **reset della stazione**, con un pulsante **per riga** nella lista:
|
||||||
|
la stazione torna senza ricette associate e si riassegna (precisazione
|
||||||
|
dell'operatore, 28/07);
|
||||||
|
- la **stazione corrente si può cambiare al volo**, per poter provare più stazioni da un
|
||||||
|
solo computer senza riconfigurare l'installazione.
|
||||||
|
|
||||||
|
**Perché conta:** è la terza richiesta di Menoncin. Il cambio al volo non è un vezzo da
|
||||||
|
sviluppatori: senza, la sessione di collaudo con Menoncin richiede tanti PC quante sono
|
||||||
|
le stazioni da provare.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Tracciabilità obbligatoria `parziale`
|
||||||
|
|
||||||
|
**Oggi:** numero di lotto e numero seriale sono facoltativi e si inseriscono nella lista
|
||||||
|
task, cioè dopo aver iniziato.
|
||||||
|
|
||||||
|
**Deve:** l'obbligatorietà di lotto e seriale si **decide alla creazione della
|
||||||
|
ricetta** (obbligatori sì/no); l'operatore li inserisce **alla selezione della
|
||||||
|
ricetta**, e finché mancano il pulsante *Avvia* **non si attiva** (precisazione
|
||||||
|
dell'operatore, 28/07).
|
||||||
|
|
||||||
|
**Perché conta:** una misura senza lotto non è tracciabile a posteriori, e la
|
||||||
|
tracciabilità è metà del valore del sistema in un audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Blocco dell'inserimento manuale `da fare`
|
||||||
|
|
||||||
|
**Oggi:** il sistema registra **come** è stata inserita una misura (calibro o tastiera),
|
||||||
|
ma accetta sempre entrambi.
|
||||||
|
|
||||||
|
**Deve:** un'impostazione della ricetta consente o vieta l'inserimento manuale, **con
|
||||||
|
divieto come impostazione predefinita**: si misura col calibro.
|
||||||
|
|
||||||
|
**Perché conta:** è il punto sollevato in call — senza questo vincolo un valore in
|
||||||
|
tolleranza si può digitare. Il dato su *come* è stata inserita c'è già: manca la regola
|
||||||
|
che lo impedisce.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Interfaccia operatore: sequenza e conferme `da fare`
|
||||||
|
|
||||||
|
Richieste del 19/05, tutte di interfaccia:
|
||||||
|
|
||||||
|
- un pulsante che **avvia i task in sequenza**, senza sceglierli a uno a uno;
|
||||||
|
- la lista completa dei task retrocessa a **secondo livello**, per tornare a vedere i
|
||||||
|
task precedenti;
|
||||||
|
- «inizia misure» rinominato **«visualizza singolo TASK»**;
|
||||||
|
- dentro il task, «Riepilogo» sostituito da **«Completato»** per passare al successivo;
|
||||||
|
- un task lasciato a metà **resta incompiuto** e si vede;
|
||||||
|
- «fine ciclo misura» **cliccabile solo quando tutte le quote hanno un valore**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Formattazione delle descrizioni `da fare`
|
||||||
|
|
||||||
|
**Deve:** le descrizioni dei task accettano andate a capo e grassetto.
|
||||||
|
|
||||||
|
**Perché conta:** chi crea le ricette fa **copia e incolla dal PDF della scheda
|
||||||
|
tecnica**; oggi il testo arriva appiattito e va risistemato a mano ogni volta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Funzionamento senza internet `da fare` · bloccante per l'installazione
|
||||||
|
|
||||||
|
**Oggi:** l'applicazione **non funziona senza collegamento a internet**. Cinque librerie
|
||||||
|
vengono scaricate al volo da servizi esterni ogni volta che si apre una pagina.
|
||||||
|
|
||||||
|
**In una rete di produzione isolata — la norma in fabbrica — il risultato è una pagina
|
||||||
|
bianca.** Non un degrado: l'interfaccia non parte proprio, e senza le altre non si vedono
|
||||||
|
i disegni tecnici né i grafici statistici.
|
||||||
|
|
||||||
|
**Deve:** tutte le librerie sono incluse nell'installazione e l'applicazione funziona a
|
||||||
|
rete staccata.
|
||||||
|
|
||||||
|
**Perché conta ora:** l'installazione a Tràfilo è on-premise e prevista per settembre.
|
||||||
|
È poco lavoro, ma va fatto **prima**, non in fabbrica il giorno dell'installazione.
|
||||||
|
|
||||||
|
**Beneficio collaterale non ovvio:** oggi una delle librerie è agganciata a una versione
|
||||||
|
"qualunque della serie 3" — cioè **l'applicazione cambia da sola** quando gli autori
|
||||||
|
pubblicano un aggiornamento, senza che nessuno l'abbia validata. Per un sistema che
|
||||||
|
produce evidenze per audit ISO 9001 / IATF 16949 questo è di per sé un problema.
|
||||||
|
Includendo le librerie le versioni si congelano: da difetto diventa argomento di vendita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Generazione dei task dalla scheda tecnica con l'AI `fuori offerta`
|
||||||
|
|
||||||
|
**Richiesta:** leggere il PDF della scheda tecnica e **creare un task per blocco**,
|
||||||
|
invece del copia-incolla manuale.
|
||||||
|
|
||||||
|
**Storia:** posta il **19/05/2026** nel documento delle modifiche, rimasta senza
|
||||||
|
risposta; **rilanciata da Tràfilo il 28/07** come elaborazione massiva iniziale delle
|
||||||
|
schede, «senza installare agenti nel sistema».
|
||||||
|
|
||||||
|
**Stato:** non è in nessuna offerta. Prima di quotare servono tre informazioni: quante
|
||||||
|
sono le schede, se il formato è standard, e se l'elaborazione è una-tantum in fase di
|
||||||
|
avviamento o una funzione permanente del prodotto. Sono domande da fare, non da
|
||||||
|
supporre — vedi Parte 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Stabilità del layout `da fare` · da circoscrivere
|
||||||
|
|
||||||
|
**Oggi:** le dimensioni delle viste **cambiano a seconda del menu**: passando da una
|
||||||
|
schermata all'altra la finestra non mantiene proporzioni stabili.
|
||||||
|
|
||||||
|
**Deve:** il layout resta stabile nel passaggio fra le viste.
|
||||||
|
|
||||||
|
**Nota:** segnalazione dell'operatore del 28/07, non ancora circoscritta sul codice —
|
||||||
|
prima di intervenire va riprodotta e va stilato l'elenco delle viste interessate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Statistica: si registra sempre, si consulta a parte `già fatto` · da confermare sul campo
|
||||||
|
|
||||||
|
**Richiesta (28/07):** a fine misura l'operatore **non va portato nella pagina della
|
||||||
|
statistica**. I dati **entrano comunque in statistica**: cambia solo chi la consulta —
|
||||||
|
serve l'**utente con il ruolo adeguato**, che apre la pagina dedicata.
|
||||||
|
|
||||||
|
**Verificato sul codice:** è già così. Tutte le pagine di statistica richiedono il
|
||||||
|
ruolo **Metrologo** (`role_required("Metrologist")` su ogni route), e a fine ciclo
|
||||||
|
l'operatore viene portato al **riepilogo**, non alla statistica.
|
||||||
|
|
||||||
|
**Resta da fare:** niente sviluppo; il requisito entra come **criterio di collaudo**
|
||||||
|
(l'operatore non deve poter raggiungere la statistica da nessun percorso) e va tenuto
|
||||||
|
fermo quando il punto 10 ridisegna la navigazione a fine task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Parte 2 — Dove si interviene
|
||||||
|
|
||||||
|
Riferimenti al branch `V2.0.0`, commit `2a56632`.
|
||||||
|
|
||||||
|
## Ordine consigliato
|
||||||
|
|
||||||
|
```
|
||||||
|
12 (offline) ──────────────► indipendente, si può fare subito
|
||||||
|
bloccante per l'installazione
|
||||||
|
|
||||||
|
1 (stato produzione) ──┬───► 3 (loop misura)
|
||||||
|
├───► 6 (fermo linea / fine produzione)
|
||||||
|
└───► 4 (limite tentativi)
|
||||||
|
|
||||||
|
2 (tipo task) ─────────────► 3, 10
|
||||||
|
|
||||||
|
7 (stazioni) · 8 (tracciabilità) · 9 (inserimento manuale) · 11 (formattazione)
|
||||||
|
indipendenti fra loro
|
||||||
|
|
||||||
|
5 (avanzamento in tolleranza) ──► prima verificare, forse è già a posto
|
||||||
|
|
||||||
|
14 (layout) ────────────────► prima riprodurre e circoscrivere le viste
|
||||||
|
|
||||||
|
15 (statistica riservata) ──► già a posto: solo criterio di collaudo,
|
||||||
|
da non rompere lavorando sul punto 10
|
||||||
|
```
|
||||||
|
|
||||||
|
La separazione **backend dati sul server / app di stazione in locale** (vedi
|
||||||
|
*Architettura d'installazione*) non è un punto di questa lista: è il contesto in
|
||||||
|
cui i punti 1, 3, 6 e 7 vanno progettati. In pratica: API senza stato, stato solo
|
||||||
|
nel database, niente dipendenze dal container per l'identità della stazione.
|
||||||
|
|
||||||
|
## 1 · Stato della produzione lato server
|
||||||
|
|
||||||
|
**Problema tecnico:** `task_execute.html` tiene in variabili Alpine
|
||||||
|
(`timerActive`, `timerRemaining`, `_timerInterval`, `cycleCount`, `cycleConfirmed`,
|
||||||
|
`productionStarted`, `measurements`) uno stato che deve sopravvivere alla pagina.
|
||||||
|
`goToNextTask()` fa `window.location.href` → il componente viene distrutto.
|
||||||
|
Lato server la sessione conserva soltanto `lot_number` e `serial_number`.
|
||||||
|
|
||||||
|
**Intervento:**
|
||||||
|
- nuove tabelle `production_runs` e `production_events` (avvio, ciclo completato, fermo
|
||||||
|
linea, ripresa, chiusura), con `station_id`, `recipe_version_id`, `operator_id`,
|
||||||
|
`supervisor_id` dove serve, timestamp;
|
||||||
|
- endpoint REST per aprire, interrogare e aggiornare la produzione corrente della
|
||||||
|
stazione;
|
||||||
|
- il frontend legge lo stato all'apertura di ogni pagina invece di tenerlo in memoria;
|
||||||
|
il conto alla rovescia si **ricalcola dall'orario di scadenza** salvato lato server,
|
||||||
|
non da un contatore locale;
|
||||||
|
- gli endpoint vanno progettati **senza stato in memoria di processo**: con l'app di
|
||||||
|
stazione installata su ogni PC (vedi *Architettura d'installazione*) il database è
|
||||||
|
l'unico posto condiviso.
|
||||||
|
|
||||||
|
**File:** `src/backend/models/orm/` (nuovo modulo), `src/backend/migrations/versions/`
|
||||||
|
(migrazione 005), `src/backend/api/routers/`, `src/frontend/flask_app/blueprints/measure.py`,
|
||||||
|
`src/frontend/flask_app/templates/measure/task_execute.html`.
|
||||||
|
|
||||||
|
**Fatto quando:** avviata una produzione, si naviga fra i task, si esce e si rientra, e
|
||||||
|
il timer prosegue coerente; il riavvio del browser non azzera nulla.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2 · Tipo di task
|
||||||
|
|
||||||
|
**Problema tecnico:** `RecipeTask` non ha campo tipo; il frontend decide con
|
||||||
|
`subtasks.length > 0` (`task_execute.html`, `task_list.html`).
|
||||||
|
|
||||||
|
**Intervento:** campo `type` su `recipe_tasks` con valori `note | measure | drawing`
|
||||||
|
(predisposto per `xf_compare`, `camera_measure`), migrazione con valorizzazione dei dati
|
||||||
|
esistenti secondo la regola attuale, selezione del tipo nell'editor ricetta,
|
||||||
|
sostituzione dei controlli su `subtasks.length` con il tipo.
|
||||||
|
|
||||||
|
**File:** `src/backend/models/orm/task.py`, nuova migrazione,
|
||||||
|
`src/backend/models/api/`, `src/frontend/flask_app/templates/maker/task_editor.html`,
|
||||||
|
`templates/measure/task_execute.html`, `templates/measure/task_list.html`.
|
||||||
|
|
||||||
|
**Fatto quando:** una ricetta con un task di misura ancora privo di quote si comporta da
|
||||||
|
task di misura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3 · Loop di misura
|
||||||
|
|
||||||
|
**Base già presente:** `confirmCycle()`, `startMeasurementTimer()`, `onTimerExpired()`,
|
||||||
|
`timerDisplay`, `playBuzzer()` in `task_execute.html` (righe ~955-1050). La logica è
|
||||||
|
corretta; il problema è la persistenza (punto 1) e il fatto che allo scadere non si può
|
||||||
|
riportare l'operatore sul task giusto.
|
||||||
|
|
||||||
|
**Intervento:** spostare la scadenza sul server; alla scadenza, **redirezione al task di
|
||||||
|
misura** della produzione corrente; con più task di misura far ripartire il timer al
|
||||||
|
completamento dell'**ultimo**; aggiungere «rimisura» che riapre il ciclo senza chiuderlo.
|
||||||
|
|
||||||
|
**Timer bidirezionale:** oggi il contatore **solo decrementa e si ferma a zero**
|
||||||
|
(`timerRemaining--`, poi `onTimerExpired()`); `timerDisplay` formatta minuti:secondi
|
||||||
|
dal residuo. Va esteso: sotto zero il valore continua **in negativo** e la
|
||||||
|
visualizzazione passa a "oltre da m:s", con stile in evidenza. Calcolando dal
|
||||||
|
timestamp di scadenza lato server (come sopra), il ritardo è coerente su qualunque
|
||||||
|
schermata e sopravvive al ricaricamento.
|
||||||
|
|
||||||
|
**Fatto quando:** con una ricetta a due task di misura e task documentali in coda, allo
|
||||||
|
scadere del timer l'operatore viene riportato alla misura da qualunque schermata.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4 · Limite di tentativi
|
||||||
|
|
||||||
|
**Problema tecnico:** nessun `max_retries` nel codice.
|
||||||
|
|
||||||
|
**Intervento:** campo sulla ricetta (accanto a `measurement_interval_minutes`, che segue
|
||||||
|
lo stesso schema), contatore per quota nella produzione corrente, superata la soglia
|
||||||
|
riuso del modale capoturno esistente con motivo `max_retries`.
|
||||||
|
|
||||||
|
**File:** `src/backend/models/orm/recipe.py`, migrazione,
|
||||||
|
`templates/maker/recipe_editor.html` (accanto al timer), `task_execute.html`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5 · Avanzamento in tolleranza — prima verificare
|
||||||
|
|
||||||
|
Il modale capoturno gestisce già `out_of_tolerance` (`task_execute.html`,
|
||||||
|
`openSupervisorModal`, `validateSupervisor`, endpoint
|
||||||
|
`measure.validate_supervisor`). **Prima di sviluppare, provare**: se il rifiuto blocca
|
||||||
|
l'avanzamento, il punto è chiuso. Se è solo una conferma, va reso vincolante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6 · Fermo linea e Fine produzione
|
||||||
|
|
||||||
|
**Problema tecnico:** in `task_execute.html` i due pulsanti aprono il modale e poi
|
||||||
|
ricadono su un commento: `fermo_linea and fine_produzione are handled by GAIA
|
||||||
|
integration (future)`. `startProduction()` è un `TODO` con la chiamata commentata.
|
||||||
|
|
||||||
|
**Intervento (senza gestionale):** scrivere gli eventi su `production_events`,
|
||||||
|
sospendere e riprendere il timer, chiudere la produzione, ed **emettere il file di
|
||||||
|
statistica** con tutte le misure della produzione. L'invio al gestionale resta un
|
||||||
|
adattatore separato da riempire quando il protocollo sarà definito (Parte 3): va
|
||||||
|
previsto il punto d'innesto, non l'implementazione.
|
||||||
|
|
||||||
|
**Fatto quando:** *fermo linea* congela il timer e solo il capoturno lo riattiva; *fine
|
||||||
|
produzione* chiude e produce il file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7 · Stazioni
|
||||||
|
|
||||||
|
**Base presente:** `Station` e `StationRecipeAssignment` (`models/orm/station.py`,
|
||||||
|
migrazione 002).
|
||||||
|
|
||||||
|
**Intervento:** ricette collegate nella lista stazioni; pulsante di reset **per riga**
|
||||||
|
che rimuove le assegnazioni; selezione della stazione corrente da parametro URL con
|
||||||
|
ricaduta sulla variabile d'ambiente, per il collaudo da una sola macchina.
|
||||||
|
|
||||||
|
**Nota (28/07, chiude il dubbio che era in D-3):** in produzione l'identità della
|
||||||
|
stazione è data dall'**installazione locale** dell'app di stazione, non dal container.
|
||||||
|
Il cambio al volo via URL resta come strumento di **collaudo**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8 · Tracciabilità
|
||||||
|
|
||||||
|
**Base presente:** `lot_number` e `serial_number` su `Measurement`, salvataggio in
|
||||||
|
sessione (`measure.save_traceability`), inserimento in `task_list.html`.
|
||||||
|
|
||||||
|
**Intervento:** flag sulla ricetta (`richiede lotto`, `richiede seriale`); spostare
|
||||||
|
l'inserimento sull'avvio produzione; *Avvia* disabilitato finché mancano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9 · Inserimento manuale
|
||||||
|
|
||||||
|
**Base presente:** `Measurement.input_method` (`manual | usb_caliper`), valorizzato dal
|
||||||
|
frontend.
|
||||||
|
|
||||||
|
**Intervento:** flag sulla ricetta `consente inserimento manuale`, **predefinito falso**;
|
||||||
|
validazione **lato server** — un flag solo nel frontend non protegge da nulla; tastierino
|
||||||
|
nascosto quando vietato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10 · Interfaccia operatore
|
||||||
|
|
||||||
|
Interventi su `templates/measure/task_list.html` e `task_execute.html`: pulsante di
|
||||||
|
avvio in sequenza, retrocessione della lista a secondo livello, rinomina dei due
|
||||||
|
pulsanti, stato «incompiuto» sui task abbandonati, «fine ciclo misura» abilitato solo a
|
||||||
|
quote complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11 · Formattazione descrizioni
|
||||||
|
|
||||||
|
`RecipeTask.description` è già `Text`. Serve un editor minimale (grassetto e a capo) e
|
||||||
|
la resa corrispondente in esecuzione, con **sanificazione dell'HTML** in ingresso.
|
||||||
|
|
||||||
|
**File:** `templates/maker/task_editor.html`, `templates/measure/task_execute.html`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12 · Funzionamento senza internet
|
||||||
|
|
||||||
|
**Verificato sul codice.** Cinque librerie esterne in sei template:
|
||||||
|
|
||||||
|
| Libreria | Dove | Senza rete si perde |
|
||||||
|
|---|---|---|
|
||||||
|
| Alpine.js `3.x.x` | `base.html` | **tutta l'interfaccia** |
|
||||||
|
| Plotly `2.32.0` | `statistics/dashboard.html` | carte di controllo e istogrammi |
|
||||||
|
| PDF.js `3.11.174` | `task_execute`, `recipe_preview`, `task_drawing`, `task_editor` | visualizzazione dei disegni |
|
||||||
|
| Fabric.js `5.3.1` | `maker/task_drawing.html` | editor delle annotazioni |
|
||||||
|
| Google Fonts | `base.html` | estetica e attese al caricamento |
|
||||||
|
|
||||||
|
**Intervento:** scaricare le librerie in `src/frontend/flask_app/static/vendor/` (la
|
||||||
|
cartella **esiste già ed è vuota**), ripuntare i tag, chiudere la policy di sicurezza in
|
||||||
|
`security_headers.py` da elenco-di-CDN a solo-origine-locale.
|
||||||
|
|
||||||
|
⚠️ **Trappola da non mancare:** PDF.js ha una **seconda** referenza al CDN,
|
||||||
|
`pdfjsLib.GlobalWorkerOptions.workerSrc`, presente in **quattro file**. Ripuntando solo
|
||||||
|
lo script principale la libreria si carica in locale **e il worker continua a cercare
|
||||||
|
internet**: sembra funzionare finché non si apre un disegno.
|
||||||
|
|
||||||
|
**Fatto quando:** con la rete staccata si percorre login → scelta ricetta → esecuzione
|
||||||
|
task → annotazione disegno → statistiche → report, senza errori in console.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Parte 3 — Decisioni che non dipendono da noi
|
||||||
|
|
||||||
|
Da chiudere **prima** che i punti collegati entrino in sviluppo. Vanno girate a Tràfilo
|
||||||
|
tramite Menoncin.
|
||||||
|
|
||||||
|
| # | Domanda | Blocca | Chi risponde |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **D-1** | **Protocollo del gestionale GAIA**: come si scambiano i dati — servizi web, database condiviso, file? | avvio produzione, fermo linea, fine produzione verso il gestionale; lettura dei codici articolo per stazione | IT Tràfilo + fornitore GAIA |
|
||||||
|
| **D-2** | **Rete e credenziali** per raggiungere GAIA dal server dove sarà installato | come sopra | IT Tràfilo |
|
||||||
|
| ~~**D-3**~~ | ~~Una applicazione per stazione o una sola per tutte?~~ **Decisa il 28/07** lato Tielogic: backend dati sul server, app di stazione installata su ogni PC — vedi *Architettura d'installazione*. Resta la validazione con l'IT di Tràfilo (macchina e rete) | — | chiusa (noi); validazione in D-4 |
|
||||||
|
| **D-4** | **Server**: quale macchina, quanto spazio disco. Sul server stanno database **e file dei disegni** (`uploads/`); l'elaborazione visione **non** è sul server (sta sull'app di stazione), quindi pesa lo spazio disco, non la potenza di calcolo | installazione | IT Tràfilo |
|
||||||
|
| **D-5** | **Cicalino**: basta il suono del browser o serve una segnalazione luminosa? La specifica chiede luce **e** suono accesi per tutta la misura, visibili da lontano. Con l'architettura del 28/07 l'app di stazione **può pilotare una colonnina**: la domanda diventa *quale hardware* | punto 3 | Tràfilo |
|
||||||
|
| **D-6** | **Autorizzazione capoturno**: username e password come oggi, o PIN rapido / badge? Venti volte al giorno la password è un attrito | punti 4, 5, 6 | Tràfilo |
|
||||||
|
| **D-7** | **Numero di tentativi** consentiti prima del capoturno: quanti, e uguali per tutte le ricette? | punto 4 | Tràfilo |
|
||||||
|
| **D-8** | **Schede tecniche**: quante sono, il formato è standard, e serve una conversione una-tantum o una funzione permanente? | punto 13 e la sua quotazione | Tràfilo |
|
||||||
|
| **D-9** | Modificare i parametri di una ricetta (timer, tentativi) **crea una nuova versione** o no? Sono parametri di esercizio, non di prodotto | punti 4, 3 | noi, con conferma cliente |
|
||||||
|
|
||||||
|
**Nota su D-1 e D-2:** finché non hanno risposta, del gestionale si può solo predisporre
|
||||||
|
il punto d'innesto. Tutto il resto del punto 6 — sospendere, riprendere, chiudere,
|
||||||
|
registrare, produrre il file — **si fa comunque e va fatto adesso**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendice — Come è stato verificato
|
||||||
|
|
||||||
|
Ogni «già fatto» e ogni «da fare» viene dalla lettura del codice al commit `2a56632`,
|
||||||
|
non dai documenti. In particolare:
|
||||||
|
|
||||||
|
- il **tipo di task** è dedotto da `subtasks.length > 0` in `task_execute.html:117` e
|
||||||
|
`task_list.html:169`;
|
||||||
|
- il **ruolo capoturno** esiste come `Supervisor`
|
||||||
|
(`api/middleware/api_key.py:71`, `blueprints/measure.py:350`);
|
||||||
|
- **fermo linea / fine produzione / avvio produzione** hanno interfaccia e gate ma
|
||||||
|
nessun effetto (`task_execute.html:1017-1021`, `1118`);
|
||||||
|
- il **timer** è un `setInterval` locale alla pagina (`task_execute.html:968-995`) e la
|
||||||
|
navigazione fra task è un ricaricamento (`task_execute.html:1054`);
|
||||||
|
- **`max_retries`** e **`production_events`** non compaiono in nessun file;
|
||||||
|
- le **cinque librerie da CDN** sono ai riferimenti citati al punto 12, e
|
||||||
|
`static/vendor/` contiene solo `.gitkeep`;
|
||||||
|
- i **disegni stanno su filesystem**, non nel database: cartella `uploads/` servita
|
||||||
|
dal backend (`api/routers/files.py:201`, `FileResponse`) — verifica del 28/07 sera,
|
||||||
|
corregge quanto detto in call;
|
||||||
|
- la **statistica è già riservata**: ogni route di `blueprints/statistics.py` porta
|
||||||
|
`@role_required("Metrologist")`; a fine ciclo il frontend va al riepilogo
|
||||||
|
(`task_execute.html:1126`), non alla statistica;
|
||||||
|
- il **timer si ferma a zero**: decremento in `task_execute.html:974-975` con uscita
|
||||||
|
su `onTimerExpired()` — il conteggio del ritardo (punto 3) oggi non esiste;
|
||||||
|
- lo stack attuale è a 4 container (`docker-compose.yml`): MySQL 8, backend FastAPI,
|
||||||
|
frontend Flask, nginx — base della sezione *Architettura d'installazione*.
|
||||||
|
|
||||||
|
Il punto 14 (layout) è l'unico **non verificato sul codice**: è una segnalazione
|
||||||
|
dell'operatore del 28/07, da riprodurre.
|
||||||
@@ -10,16 +10,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
|
|
||||||
from src.backend.config import settings
|
from src.backend.config import settings
|
||||||
|
|
||||||
# Content Security Policy - allows CDN resources used by the client
|
# Content Security Policy - same-origin only: every third-party library now ships with
|
||||||
|
# the app (see frontend static/vendor/), so no CDN host needs allowing. The install runs
|
||||||
|
# on an isolated shop-floor network where an outbound fetch would simply fail.
|
||||||
# Note: 'unsafe-eval' required for Plotly.js runtime evaluation in SPC charts
|
# Note: 'unsafe-eval' required for Plotly.js runtime evaluation in SPC charts
|
||||||
CSP = (
|
CSP = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' "
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||||
"https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://cdn.plot.ly; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
|
"font-src 'self'; "
|
||||||
"font-src 'self' https://fonts.gstatic.com; "
|
|
||||||
"img-src 'self' data: blob:; "
|
"img-src 'self' data: blob:; "
|
||||||
"connect-src 'self'"
|
"connect-src 'self'; "
|
||||||
|
"worker-src 'self' blob:"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""Production runs router - open, read, and drive the life of a production.
|
||||||
|
|
||||||
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.backend.api.middleware.api_key import get_current_user
|
||||||
|
from src.backend.database import get_db
|
||||||
|
from src.backend.models.api.production import (
|
||||||
|
CycleCompletePayload,
|
||||||
|
ProductionEventResponse,
|
||||||
|
ProductionRunCreate,
|
||||||
|
ProductionRunResponse,
|
||||||
|
ProductionRunWithEventsResponse,
|
||||||
|
SupervisorAction,
|
||||||
|
)
|
||||||
|
from src.backend.models.orm.production import ProductionRun
|
||||||
|
from src.backend.models.orm.user import User
|
||||||
|
from src.backend.services import production_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/production-runs", tags=["production"])
|
||||||
|
|
||||||
|
|
||||||
|
def _as_response(run: ProductionRun) -> ProductionRunResponse:
|
||||||
|
return ProductionRunResponse(
|
||||||
|
**ProductionRunResponse.model_validate(run).model_dump(
|
||||||
|
exclude={"seconds_to_next_measurement", "overdue", "server_time"}
|
||||||
|
),
|
||||||
|
**production_service.describe(run),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ProductionRunResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def open_production_run(
|
||||||
|
data: ProductionRunCreate,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Start a production at a station."""
|
||||||
|
run = await production_service.open_run(db, data, user)
|
||||||
|
return _as_response(run)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current", response_model=ProductionRunResponse | None)
|
||||||
|
async def get_current_production_run(
|
||||||
|
station_code: str = Query(..., min_length=1),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""The run open at this station, or null.
|
||||||
|
|
||||||
|
This is what every page asks on load instead of keeping the timer in memory.
|
||||||
|
"""
|
||||||
|
station = await production_service.get_station_by_code(db, station_code)
|
||||||
|
run = await production_service.get_open_run_for_station(db, station.id)
|
||||||
|
return _as_response(run) if run is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{run_id}", response_model=ProductionRunWithEventsResponse)
|
||||||
|
async def get_production_run(
|
||||||
|
run_id: int,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""A run with its full trace - the history a production did not have before."""
|
||||||
|
run = await production_service.get_run(db, run_id)
|
||||||
|
events = await production_service.list_run_events(db, run_id)
|
||||||
|
return ProductionRunWithEventsResponse(
|
||||||
|
**_as_response(run).model_dump(),
|
||||||
|
events=[ProductionEventResponse.model_validate(e) for e in events],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{run_id}/cycle", response_model=ProductionRunResponse)
|
||||||
|
async def complete_measurement_cycle(
|
||||||
|
run_id: int,
|
||||||
|
payload: CycleCompletePayload | None = None,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Record a finished measurement cycle and restart the interval."""
|
||||||
|
run = await production_service.get_run(db, run_id)
|
||||||
|
run = await production_service.complete_cycle(
|
||||||
|
db, run, user, note=payload.note if payload else None,
|
||||||
|
)
|
||||||
|
return _as_response(run)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{run_id}/pause", response_model=ProductionRunResponse)
|
||||||
|
async def pause_production_run(
|
||||||
|
run_id: int,
|
||||||
|
action: SupervisorAction,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Fermo linea - requires a supervisor."""
|
||||||
|
run = await production_service.get_run(db, run_id)
|
||||||
|
supervisor = await production_service.authorise_supervisor(
|
||||||
|
db, action.supervisor_username, action.supervisor_password,
|
||||||
|
)
|
||||||
|
run = await production_service.pause_run(db, run, user, supervisor, note=action.note)
|
||||||
|
return _as_response(run)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{run_id}/resume", response_model=ProductionRunResponse)
|
||||||
|
async def resume_production_run(
|
||||||
|
run_id: int,
|
||||||
|
action: SupervisorAction,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Restart a stopped line - requires a supervisor."""
|
||||||
|
run = await production_service.get_run(db, run_id)
|
||||||
|
supervisor = await production_service.authorise_supervisor(
|
||||||
|
db, action.supervisor_username, action.supervisor_password,
|
||||||
|
)
|
||||||
|
run = await production_service.resume_run(db, run, user, supervisor, note=action.note)
|
||||||
|
return _as_response(run)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{run_id}/close", response_model=ProductionRunResponse)
|
||||||
|
async def close_production_run(
|
||||||
|
run_id: int,
|
||||||
|
action: SupervisorAction,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""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(
|
||||||
|
db, action.supervisor_username, action.supervisor_password,
|
||||||
|
)
|
||||||
|
run = await production_service.close_run(db, run, user, supervisor, note=action.note)
|
||||||
|
return _as_response(run)
|
||||||
@@ -11,6 +11,8 @@ from src.backend.models.api.station import (
|
|||||||
StationResponse,
|
StationResponse,
|
||||||
StationRecipeAssignmentCreate,
|
StationRecipeAssignmentCreate,
|
||||||
StationRecipeAssignmentResponse,
|
StationRecipeAssignmentResponse,
|
||||||
|
StationResetResponse,
|
||||||
|
StationWithRecipesResponse,
|
||||||
RecipeSummary,
|
RecipeSummary,
|
||||||
)
|
)
|
||||||
from src.backend.services import station_service
|
from src.backend.services import station_service
|
||||||
@@ -18,15 +20,36 @@ from src.backend.services import station_service
|
|||||||
router = APIRouter(prefix="/api/stations", tags=["stations"])
|
router = APIRouter(prefix="/api/stations", tags=["stations"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[StationResponse])
|
@router.get("", response_model=list[StationWithRecipesResponse])
|
||||||
async def list_stations(
|
async def list_stations(
|
||||||
active_only: bool = False,
|
active_only: bool = False,
|
||||||
admin: User = Depends(require_admin_user),
|
admin: User = Depends(require_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List all stations (admin only)."""
|
"""List all stations with the recipes assigned to each (admin only).
|
||||||
|
|
||||||
|
The admin station list shows which products a station handles, so the
|
||||||
|
assignments travel with the station instead of needing a call per row.
|
||||||
|
Station.assignments is selectin-loaded, so this costs no extra query.
|
||||||
|
|
||||||
|
Only active recipes are listed, matching what an operator actually sees
|
||||||
|
at that station via /by-code/{code}/recipes.
|
||||||
|
"""
|
||||||
stations = await station_service.list_stations(db, active_only=active_only)
|
stations = await station_service.list_stations(db, active_only=active_only)
|
||||||
return [StationResponse.model_validate(s) for s in stations]
|
return [
|
||||||
|
StationWithRecipesResponse(
|
||||||
|
**StationResponse.model_validate(s).model_dump(),
|
||||||
|
recipes=sorted(
|
||||||
|
(
|
||||||
|
RecipeSummary.model_validate(a.recipe)
|
||||||
|
for a in s.assignments
|
||||||
|
if a.recipe is not None and a.recipe.active
|
||||||
|
),
|
||||||
|
key=lambda r: r.code,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for s in stations
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StationResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("", response_model=StationResponse, status_code=status.HTTP_201_CREATED)
|
||||||
@@ -128,6 +151,22 @@ async def assign_recipe_to_station(
|
|||||||
return StationRecipeAssignmentResponse.model_validate(assignment)
|
return StationRecipeAssignmentResponse.model_validate(assignment)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{station_id}/recipes", response_model=StationResetResponse)
|
||||||
|
async def reset_station_recipes(
|
||||||
|
station_id: int,
|
||||||
|
admin: User = Depends(require_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Clear every recipe assignment of a station (admin only).
|
||||||
|
|
||||||
|
Backs the per-row reset in the admin list: the station is left with no
|
||||||
|
recipes and can be reassigned from scratch, without deleting the station
|
||||||
|
itself. Idempotent - resetting an already empty station reports removed=0.
|
||||||
|
"""
|
||||||
|
removed = await station_service.unassign_all_recipes(db, station_id)
|
||||||
|
return StationResetResponse(station_id=station_id, removed=removed)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{station_id}/recipes/{recipe_id}",
|
"/{station_id}/recipes/{recipe_id}",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from src.backend.api.routers.reports import router as reports_router
|
|||||||
from src.backend.api.routers.statistics import router as statistics_router
|
from src.backend.api.routers.statistics import router as statistics_router
|
||||||
from src.backend.api.routers.setup import router as setup_router
|
from src.backend.api.routers.setup import router as setup_router
|
||||||
from src.backend.api.routers.stations import router as stations_router
|
from src.backend.api.routers.stations import router as stations_router
|
||||||
|
from src.backend.api.routers.production import router as production_router
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -73,6 +74,7 @@ app.include_router(statistics_router)
|
|||||||
app.include_router(reports_router)
|
app.include_router(reports_router)
|
||||||
app.include_router(setup_router)
|
app.include_router(setup_router)
|
||||||
app.include_router(stations_router)
|
app.include_router(stations_router)
|
||||||
|
app.include_router(production_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ from src.backend.models.orm.task import RecipeTask, RecipeSubtask # noqa: F401
|
|||||||
from src.backend.models.orm.measurement import Measurement # noqa: F401
|
from src.backend.models.orm.measurement import Measurement # noqa: F401
|
||||||
from src.backend.models.orm.access_log import AccessLog # noqa: F401
|
from src.backend.models.orm.access_log import AccessLog # noqa: F401
|
||||||
from src.backend.models.orm.setting import SystemSetting, RecipeVersionAudit # noqa: F401
|
from src.backend.models.orm.setting import SystemSetting, RecipeVersionAudit # noqa: F401
|
||||||
|
from src.backend.models.orm.station import Station, StationRecipeAssignment # noqa: F401
|
||||||
|
from src.backend.models.orm.production import ProductionRun, ProductionEvent # noqa: F401
|
||||||
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""add production_runs and production_events
|
||||||
|
|
||||||
|
Gives a production a life of its own: before this its state lived in the Alpine
|
||||||
|
component of task_execute.html, so changing task - a full page load - lost the timer,
|
||||||
|
the cycle count and the "production started" flag.
|
||||||
|
|
||||||
|
Revision ID: 005_production_runs
|
||||||
|
Revises: 004_input_duration
|
||||||
|
Create Date: 2026-07-28
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision: str = '005_production_runs'
|
||||||
|
down_revision: Union[str, None] = '004_input_duration'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'production_runs',
|
||||||
|
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
|
||||||
|
sa.Column('station_id', sa.Integer, sa.ForeignKey('stations.id'), nullable=False),
|
||||||
|
sa.Column('recipe_id', sa.Integer, sa.ForeignKey('recipes.id'), nullable=False),
|
||||||
|
sa.Column('version_id', sa.Integer, sa.ForeignKey('recipe_versions.id'), nullable=False),
|
||||||
|
sa.Column('operator_id', sa.Integer, sa.ForeignKey('users.id'), nullable=False),
|
||||||
|
sa.Column('lot_number', sa.String(100), nullable=True),
|
||||||
|
sa.Column('serial_number', sa.String(100), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
'status',
|
||||||
|
sa.Enum('running', 'paused', 'closed', name='production_run_status_enum'),
|
||||||
|
nullable=False,
|
||||||
|
server_default='running',
|
||||||
|
),
|
||||||
|
sa.Column('measurement_interval_minutes', sa.SmallInteger, nullable=True),
|
||||||
|
sa.Column('next_measurement_at', sa.DateTime, nullable=True),
|
||||||
|
sa.Column('cycle_count', sa.Integer, nullable=False, server_default='0'),
|
||||||
|
sa.Column('started_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.Column('paused_at', sa.DateTime, nullable=True),
|
||||||
|
sa.Column('closed_at', sa.DateTime, nullable=True),
|
||||||
|
sa.Column('closed_by', sa.Integer, sa.ForeignKey('users.id'), nullable=True),
|
||||||
|
# Mirrors station_id while open, NULL once closed. The unique constraint makes
|
||||||
|
# "one open run per station" a database guarantee rather than a race; repeated
|
||||||
|
# NULLs do not collide, so closed runs are free to pile up.
|
||||||
|
sa.Column('active_station_id', sa.Integer, nullable=True),
|
||||||
|
# Declared inline rather than added afterwards: adding a constraint is an ALTER,
|
||||||
|
# which SQLite cannot do, and the test databases are SQLite.
|
||||||
|
sa.UniqueConstraint('active_station_id', name='uq_production_runs_active_station'),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8mb4',
|
||||||
|
)
|
||||||
|
op.create_index('ix_production_runs_station_id', 'production_runs', ['station_id'])
|
||||||
|
op.create_index('ix_production_runs_recipe_id', 'production_runs', ['recipe_id'])
|
||||||
|
op.create_index('ix_production_runs_version_id', 'production_runs', ['version_id'])
|
||||||
|
op.create_index('ix_production_runs_operator_id', 'production_runs', ['operator_id'])
|
||||||
|
op.create_index('ix_production_runs_lot_number', 'production_runs', ['lot_number'])
|
||||||
|
op.create_index('ix_production_runs_serial_number', 'production_runs', ['serial_number'])
|
||||||
|
op.create_index('ix_production_runs_status', 'production_runs', ['status'])
|
||||||
|
op.create_index(
|
||||||
|
'ix_production_runs_next_measurement_at', 'production_runs', ['next_measurement_at'],
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
'ix_production_runs_station_status', 'production_runs', ['station_id', 'status'],
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'production_events',
|
||||||
|
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
|
||||||
|
sa.Column(
|
||||||
|
'run_id', sa.Integer,
|
||||||
|
sa.ForeignKey('production_runs.id', ondelete='CASCADE'), nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
'event_type',
|
||||||
|
sa.Enum(
|
||||||
|
'start', 'cycle_completed', 'line_stop', 'resume', 'close',
|
||||||
|
name='production_event_type_enum',
|
||||||
|
),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id'), nullable=False),
|
||||||
|
sa.Column('supervisor_id', sa.Integer, sa.ForeignKey('users.id'), nullable=True),
|
||||||
|
sa.Column('note', sa.Text, nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8mb4',
|
||||||
|
)
|
||||||
|
op.create_index('ix_production_events_run_id', 'production_events', ['run_id'])
|
||||||
|
op.create_index('ix_production_events_event_type', 'production_events', ['event_type'])
|
||||||
|
op.create_index('ix_production_events_user_id', 'production_events', ['user_id'])
|
||||||
|
op.create_index('ix_production_events_created_at', 'production_events', ['created_at'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('production_events')
|
||||||
|
op.drop_table('production_runs')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Pydantic schemas for production runs and their events."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionRunCreate(BaseModel):
|
||||||
|
station_code: str = Field(..., min_length=1, max_length=100)
|
||||||
|
recipe_id: int = Field(..., gt=0)
|
||||||
|
version_id: Optional[int] = Field(default=None, gt=0)
|
||||||
|
lot_number: Optional[str] = Field(default=None, max_length=100)
|
||||||
|
serial_number: Optional[str] = Field(default=None, max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionEventResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: int
|
||||||
|
event_type: str
|
||||||
|
user_id: int
|
||||||
|
supervisor_id: Optional[int]
|
||||||
|
note: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionRunResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
station_id: int
|
||||||
|
recipe_id: int
|
||||||
|
version_id: int
|
||||||
|
operator_id: int
|
||||||
|
lot_number: Optional[str]
|
||||||
|
serial_number: Optional[str]
|
||||||
|
status: str
|
||||||
|
measurement_interval_minutes: Optional[int]
|
||||||
|
next_measurement_at: Optional[datetime]
|
||||||
|
cycle_count: int
|
||||||
|
started_at: datetime
|
||||||
|
paused_at: Optional[datetime]
|
||||||
|
closed_at: Optional[datetime]
|
||||||
|
closed_by: Optional[int]
|
||||||
|
|
||||||
|
# Derived server-side so every client agrees on the countdown regardless of
|
||||||
|
# clock skew. Negative once the interval has elapsed: how long the run has been
|
||||||
|
# overdue is a fact the operator must see, not deduce.
|
||||||
|
seconds_to_next_measurement: Optional[int] = None
|
||||||
|
overdue: bool = False
|
||||||
|
server_time: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionRunWithEventsResponse(ProductionRunResponse):
|
||||||
|
events: list[ProductionEventResponse] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class SupervisorAction(BaseModel):
|
||||||
|
"""Credentials of the supervisor authorising a line stop or a close."""
|
||||||
|
|
||||||
|
supervisor_username: str = Field(..., min_length=1)
|
||||||
|
supervisor_password: str = Field(..., min_length=1)
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CycleCompletePayload(BaseModel):
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
ProductionEventType = Literal[
|
||||||
|
"start", "cycle_completed", "line_stop", "resume", "close",
|
||||||
|
]
|
||||||
@@ -57,3 +57,10 @@ class RecipeSummary(BaseModel):
|
|||||||
|
|
||||||
class StationWithRecipesResponse(StationResponse):
|
class StationWithRecipesResponse(StationResponse):
|
||||||
recipes: list[RecipeSummary] = Field(default_factory=list)
|
recipes: list[RecipeSummary] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class StationResetResponse(BaseModel):
|
||||||
|
"""Outcome of clearing every recipe assignment of a station."""
|
||||||
|
|
||||||
|
station_id: int
|
||||||
|
removed: int
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from src.backend.models.orm.measurement import Measurement
|
|||||||
from src.backend.models.orm.access_log import AccessLog
|
from src.backend.models.orm.access_log import AccessLog
|
||||||
from src.backend.models.orm.setting import SystemSetting, RecipeVersionAudit
|
from src.backend.models.orm.setting import SystemSetting, RecipeVersionAudit
|
||||||
from src.backend.models.orm.station import Station, StationRecipeAssignment
|
from src.backend.models.orm.station import Station, StationRecipeAssignment
|
||||||
|
from src.backend.models.orm.production import ProductionRun, ProductionEvent
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"User",
|
"User",
|
||||||
@@ -19,4 +20,6 @@ __all__ = [
|
|||||||
"RecipeVersionAudit",
|
"RecipeVersionAudit",
|
||||||
"Station",
|
"Station",
|
||||||
"StationRecipeAssignment",
|
"StationRecipeAssignment",
|
||||||
|
"ProductionRun",
|
||||||
|
"ProductionEvent",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""ProductionRun and ProductionEvent models.
|
||||||
|
|
||||||
|
A production run is what an operator opens when production starts and a supervisor
|
||||||
|
closes when it ends. It is the thing that outlives a page: before this, the timer,
|
||||||
|
the cycle count and the "production started" flag were Alpine variables in
|
||||||
|
task_execute.html, and navigating between tasks - a full page load - destroyed them.
|
||||||
|
|
||||||
|
The measurement deadline is stored as an absolute timestamp (next_measurement_at)
|
||||||
|
rather than a remaining count. Any client can then derive the countdown from it, get
|
||||||
|
the same answer, survive a reload, and tell how far *past* the interval it is by
|
||||||
|
letting the difference go negative.
|
||||||
|
|
||||||
|
With one station app installed per PC (see the install architecture of 28/07) the
|
||||||
|
database is the only place this state can live: the API keeps nothing in process
|
||||||
|
memory.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime, Enum, ForeignKey, Index, Integer, SmallInteger, String, Text,
|
||||||
|
UniqueConstraint, func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.backend.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.backend.models.orm.recipe import Recipe, RecipeVersion
|
||||||
|
from src.backend.models.orm.station import Station
|
||||||
|
|
||||||
|
|
||||||
|
RUN_STATUSES = ("running", "paused", "closed")
|
||||||
|
EVENT_TYPES = ("start", "cycle_completed", "line_stop", "resume", "close")
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionRun(Base):
|
||||||
|
__tablename__ = "production_runs"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
station_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("stations.id"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
recipe_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("recipes.id"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
version_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("recipe_versions.id"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
operator_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Traceability travels with the run instead of the Flask session, so it stays
|
||||||
|
# attached to every measurement taken during it.
|
||||||
|
lot_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
||||||
|
serial_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
||||||
|
|
||||||
|
status: Mapped[str] = mapped_column(
|
||||||
|
Enum(*RUN_STATUSES, name="production_run_status_enum"),
|
||||||
|
nullable=False,
|
||||||
|
default="running",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Snapshot of the recipe interval at start: editing the recipe mid-run must not
|
||||||
|
# move the deadline of a production already under way.
|
||||||
|
measurement_interval_minutes: Mapped[Optional[int]] = mapped_column(
|
||||||
|
SmallInteger, nullable=True
|
||||||
|
)
|
||||||
|
# When the next measurement falls due. Null when the recipe has no interval.
|
||||||
|
# Past this instant the run is overdue, and by how much is simply now - this.
|
||||||
|
next_measurement_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
DateTime, nullable=True, index=True
|
||||||
|
)
|
||||||
|
cycle_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
started_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||||
|
closed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||||
|
closed_by: Mapped[Optional[int]] = mapped_column(
|
||||||
|
Integer, ForeignKey("users.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mirrors station_id while the run is open and goes NULL when it closes. A unique
|
||||||
|
# index on it lets the database - not a check-then-insert race - guarantee that a
|
||||||
|
# station never has two open runs. Repeated NULLs do not collide in a unique index.
|
||||||
|
active_station_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||||
|
|
||||||
|
station: Mapped["Station"] = relationship(lazy="selectin")
|
||||||
|
recipe: Mapped["Recipe"] = relationship(lazy="selectin")
|
||||||
|
version: Mapped["RecipeVersion"] = relationship(lazy="selectin")
|
||||||
|
events: Mapped[list["ProductionEvent"]] = relationship(
|
||||||
|
back_populates="run",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
order_by="ProductionEvent.created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("active_station_id", name="uq_production_runs_active_station"),
|
||||||
|
Index("ix_production_runs_station_status", "station_id", "status"),
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_open(self) -> bool:
|
||||||
|
return self.status in ("running", "paused")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<ProductionRun {self.id} station={self.station_id} {self.status}>"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionEvent(Base):
|
||||||
|
"""Append-only trace of what happened during a run.
|
||||||
|
|
||||||
|
This is the record that did not exist before: without it there is no history of
|
||||||
|
a production, and nothing for the statistics file or the ERP hand-off to stand on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "production_events"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
run_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("production_runs.id", ondelete="CASCADE"),
|
||||||
|
nullable=False, index=True,
|
||||||
|
)
|
||||||
|
event_type: Mapped[str] = mapped_column(
|
||||||
|
Enum(*EVENT_TYPES, name="production_event_type_enum"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
user_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
# Who authorised it, when the action needed a supervisor (fermo linea, chiusura).
|
||||||
|
supervisor_id: Mapped[Optional[int]] = mapped_column(
|
||||||
|
Integer, ForeignKey("users.id"), nullable=True
|
||||||
|
)
|
||||||
|
note: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, nullable=False, server_default=func.now(), index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
run: Mapped["ProductionRun"] = relationship(back_populates="events")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<ProductionEvent run={self.run_id} {self.event_type}>"
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"""Business logic for production runs.
|
||||||
|
|
||||||
|
Routers must call into these functions rather than manipulating models directly.
|
||||||
|
All functions are async and accept an AsyncSession; they flush but do NOT commit
|
||||||
|
(commit is handled by the FastAPI get_db dependency).
|
||||||
|
|
||||||
|
Clock convention
|
||||||
|
----------------
|
||||||
|
Every timestamp this module writes comes from ``_now()``, so the deadline and the
|
||||||
|
comparison against it are always on the same clock. Clients are handed
|
||||||
|
``seconds_to_next_measurement`` already computed rather than the raw deadline: a
|
||||||
|
naive datetime crossing the wire would be parsed in the browser's own timezone, and
|
||||||
|
the countdown would be off by the UTC offset. The number is the contract; the
|
||||||
|
timestamp is only there for the audit trail.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import select
|
||||||
|
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.user import User
|
||||||
|
from src.backend.services import auth_service
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
"""Single source of time for runs.
|
||||||
|
|
||||||
|
Naive local time, matching what the database writes for ``func.now()`` defaults,
|
||||||
|
so a run's started_at and its deadline are comparable.
|
||||||
|
"""
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Derived view
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def seconds_to_next_measurement(run: ProductionRun, now: Optional[datetime] = None) -> Optional[int]:
|
||||||
|
"""Seconds until the next measurement is due; negative once it is overdue.
|
||||||
|
|
||||||
|
Letting the value go negative is deliberate: the operator has to see how long
|
||||||
|
the line has been past the interval, not merely that it elapsed.
|
||||||
|
While the run is paused the countdown is frozen at what was left when the line
|
||||||
|
stopped - a fermo linea must not eat into the measurement interval.
|
||||||
|
"""
|
||||||
|
if run.next_measurement_at is None:
|
||||||
|
return None
|
||||||
|
reference = run.paused_at if (run.status == "paused" and run.paused_at) else (now or _now())
|
||||||
|
return int(round((run.next_measurement_at - reference).total_seconds()))
|
||||||
|
|
||||||
|
|
||||||
|
def describe(run: ProductionRun) -> dict:
|
||||||
|
"""Fields the API adds on top of the stored columns."""
|
||||||
|
now = _now()
|
||||||
|
remaining = seconds_to_next_measurement(run, now)
|
||||||
|
return {
|
||||||
|
"seconds_to_next_measurement": remaining,
|
||||||
|
"overdue": remaining is not None and remaining < 0,
|
||||||
|
"server_time": now,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lookups
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def get_run(db: AsyncSession, run_id: int) -> ProductionRun:
|
||||||
|
result = await db.execute(select(ProductionRun).where(ProductionRun.id == run_id))
|
||||||
|
run = result.scalar_one_or_none()
|
||||||
|
if run is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Production run not found",
|
||||||
|
)
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
async def get_open_run_for_station(
|
||||||
|
db: AsyncSession, station_id: int,
|
||||||
|
) -> Optional[ProductionRun]:
|
||||||
|
"""The run currently open at a station, running or paused."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(ProductionRun).where(ProductionRun.active_station_id == station_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_station_by_code(db: AsyncSession, code: str) -> Station:
|
||||||
|
result = await db.execute(select(Station).where(Station.code == code))
|
||||||
|
station = result.scalar_one_or_none()
|
||||||
|
if station is None or not station.active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Station '{code}' not found or inactive",
|
||||||
|
)
|
||||||
|
return station
|
||||||
|
|
||||||
|
|
||||||
|
async def list_run_events(db: AsyncSession, run_id: int) -> list[ProductionEvent]:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ProductionEvent)
|
||||||
|
.where(ProductionEvent.run_id == run_id)
|
||||||
|
.order_by(ProductionEvent.created_at, ProductionEvent.id)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Supervisor authorisation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def authorise_supervisor(db: AsyncSession, username: str, password: str) -> User:
|
||||||
|
"""Check the credentials of the supervisor authorising a stop or a close.
|
||||||
|
|
||||||
|
Uses authenticate_user rather than a full login: logging in would rotate the
|
||||||
|
supervisor's API key and knock out whatever session they have open elsewhere.
|
||||||
|
"""
|
||||||
|
supervisor = await auth_service.authenticate_user(db, username, password)
|
||||||
|
if supervisor is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid supervisor credentials",
|
||||||
|
)
|
||||||
|
if not supervisor.has_role("Supervisor") and not supervisor.is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="User is not authorised as supervisor",
|
||||||
|
)
|
||||||
|
return supervisor
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _add_event(
|
||||||
|
db: AsyncSession,
|
||||||
|
run: ProductionRun,
|
||||||
|
event_type: str,
|
||||||
|
user: User,
|
||||||
|
supervisor: Optional[User] = None,
|
||||||
|
note: Optional[str] = None,
|
||||||
|
) -> ProductionEvent:
|
||||||
|
"""Append to the run's trace.
|
||||||
|
|
||||||
|
Added through the session rather than run.events: touching the collection would
|
||||||
|
trigger a lazy load, which raises under async SQLAlchemy.
|
||||||
|
"""
|
||||||
|
event = ProductionEvent(
|
||||||
|
run_id=run.id,
|
||||||
|
event_type=event_type,
|
||||||
|
user_id=user.id,
|
||||||
|
supervisor_id=supervisor.id if supervisor else None,
|
||||||
|
note=note,
|
||||||
|
created_at=_now(),
|
||||||
|
)
|
||||||
|
db.add(event)
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
async def open_run(
|
||||||
|
db: AsyncSession, data: ProductionRunCreate, operator: User,
|
||||||
|
) -> ProductionRun:
|
||||||
|
"""Start a production at a station.
|
||||||
|
|
||||||
|
Refuses if the station already has one open: two runs on the same station would
|
||||||
|
mean two timers and two histories for one physical line.
|
||||||
|
"""
|
||||||
|
station = await get_station_by_code(db, data.station_code)
|
||||||
|
|
||||||
|
existing = await get_open_run_for_station(db, station.id)
|
||||||
|
if existing is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=f"Station '{station.code}' already has an open production run",
|
||||||
|
)
|
||||||
|
|
||||||
|
recipe_row = await db.execute(select(Recipe).where(Recipe.id == data.recipe_id))
|
||||||
|
recipe = recipe_row.scalar_one_or_none()
|
||||||
|
if recipe is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Recipe not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.version_id is not None:
|
||||||
|
version_row = await db.execute(
|
||||||
|
select(RecipeVersion).where(
|
||||||
|
RecipeVersion.id == data.version_id,
|
||||||
|
RecipeVersion.recipe_id == recipe.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
version = version_row.scalar_one_or_none()
|
||||||
|
if version is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Recipe version not found for this recipe",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
version_row = await db.execute(
|
||||||
|
select(RecipeVersion).where(
|
||||||
|
RecipeVersion.recipe_id == recipe.id,
|
||||||
|
RecipeVersion.is_current == True, # noqa: E712
|
||||||
|
)
|
||||||
|
)
|
||||||
|
version = version_row.scalar_one_or_none()
|
||||||
|
if version is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Recipe has no current version",
|
||||||
|
)
|
||||||
|
|
||||||
|
now = _now()
|
||||||
|
# The interval is copied, not referenced: editing the recipe mid-production must
|
||||||
|
# not move the deadline of a run already under way.
|
||||||
|
interval = recipe.measurement_interval_minutes
|
||||||
|
run = ProductionRun(
|
||||||
|
station_id=station.id,
|
||||||
|
recipe_id=recipe.id,
|
||||||
|
version_id=version.id,
|
||||||
|
operator_id=operator.id,
|
||||||
|
lot_number=data.lot_number,
|
||||||
|
serial_number=data.serial_number,
|
||||||
|
status="running",
|
||||||
|
measurement_interval_minutes=interval,
|
||||||
|
next_measurement_at=(
|
||||||
|
now + timedelta(minutes=interval) if interval and interval > 0 else None
|
||||||
|
),
|
||||||
|
cycle_count=0,
|
||||||
|
started_at=now,
|
||||||
|
active_station_id=station.id,
|
||||||
|
)
|
||||||
|
db.add(run)
|
||||||
|
await db.flush()
|
||||||
|
_add_event(db, run, "start", operator)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(run)
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
def _require_open(run: ProductionRun) -> None:
|
||||||
|
if run.status == "closed":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Production run is already closed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def complete_cycle(
|
||||||
|
db: AsyncSession, run: ProductionRun, user: User, note: Optional[str] = None,
|
||||||
|
) -> ProductionRun:
|
||||||
|
"""Record a finished measurement cycle and restart the interval."""
|
||||||
|
_require_open(run)
|
||||||
|
if run.status == "paused":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Production run is paused: resume it before measuring",
|
||||||
|
)
|
||||||
|
|
||||||
|
now = _now()
|
||||||
|
run.cycle_count += 1
|
||||||
|
if run.measurement_interval_minutes and run.measurement_interval_minutes > 0:
|
||||||
|
# Measured from now, not from the previous deadline: a late measurement must
|
||||||
|
# not compress the following interval.
|
||||||
|
run.next_measurement_at = now + timedelta(
|
||||||
|
minutes=run.measurement_interval_minutes
|
||||||
|
)
|
||||||
|
_add_event(db, run, "cycle_completed", user, note=note)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(run)
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
async def pause_run(
|
||||||
|
db: AsyncSession,
|
||||||
|
run: ProductionRun,
|
||||||
|
user: User,
|
||||||
|
supervisor: User,
|
||||||
|
note: Optional[str] = None,
|
||||||
|
) -> ProductionRun:
|
||||||
|
"""Fermo linea: suspend the run and freeze the countdown."""
|
||||||
|
_require_open(run)
|
||||||
|
if run.status == "paused":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Production run is already paused",
|
||||||
|
)
|
||||||
|
run.status = "paused"
|
||||||
|
run.paused_at = _now()
|
||||||
|
_add_event(db, run, "line_stop", user, supervisor=supervisor, note=note)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(run)
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
async def resume_run(
|
||||||
|
db: AsyncSession,
|
||||||
|
run: ProductionRun,
|
||||||
|
user: User,
|
||||||
|
supervisor: User,
|
||||||
|
note: Optional[str] = None,
|
||||||
|
) -> ProductionRun:
|
||||||
|
"""Restart a paused run, pushing the deadline out by the length of the stop."""
|
||||||
|
_require_open(run)
|
||||||
|
if run.status != "paused":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Production run is not paused",
|
||||||
|
)
|
||||||
|
now = _now()
|
||||||
|
if run.next_measurement_at is not None and run.paused_at is not None:
|
||||||
|
# Shift rather than recompute: whatever was left when the line stopped is
|
||||||
|
# what the operator gets back, so a stop neither grants nor costs time.
|
||||||
|
run.next_measurement_at = run.next_measurement_at + (now - run.paused_at)
|
||||||
|
run.status = "running"
|
||||||
|
run.paused_at = None
|
||||||
|
_add_event(db, run, "resume", user, supervisor=supervisor, note=note)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(run)
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
async def close_run(
|
||||||
|
db: AsyncSession,
|
||||||
|
run: ProductionRun,
|
||||||
|
user: User,
|
||||||
|
supervisor: User,
|
||||||
|
note: Optional[str] = None,
|
||||||
|
) -> ProductionRun:
|
||||||
|
"""Fine produzione: close the run for good and stop the timer."""
|
||||||
|
_require_open(run)
|
||||||
|
run.status = "closed"
|
||||||
|
run.closed_at = _now()
|
||||||
|
run.closed_by = supervisor.id
|
||||||
|
run.next_measurement_at = None
|
||||||
|
run.paused_at = None
|
||||||
|
# Releasing the slot lets the station open a new run; the unique index on this
|
||||||
|
# column is what keeps exactly one open at a time.
|
||||||
|
run.active_station_id = None
|
||||||
|
_add_event(db, run, "close", user, supervisor=supervisor, note=note)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(run)
|
||||||
|
return run
|
||||||
@@ -138,6 +138,26 @@ async def unassign_recipe(
|
|||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
|
|
||||||
|
async def unassign_all_recipes(db: AsyncSession, station_id: int) -> int:
|
||||||
|
"""Drop every recipe assignment of a station and return how many were removed.
|
||||||
|
|
||||||
|
Used by the per-row reset in the admin station list: the station goes back to
|
||||||
|
having no recipes so it can be reassigned from scratch. Unlike unassign_recipe
|
||||||
|
this is idempotent - resetting an already empty station returns 0 rather than 404.
|
||||||
|
"""
|
||||||
|
await get_station(db, station_id)
|
||||||
|
result = await db.execute(
|
||||||
|
select(StationRecipeAssignment).where(
|
||||||
|
StationRecipeAssignment.station_id == station_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assignments = list(result.scalars().all())
|
||||||
|
for assignment in assignments:
|
||||||
|
await db.delete(assignment)
|
||||||
|
await db.flush()
|
||||||
|
return len(assignments)
|
||||||
|
|
||||||
|
|
||||||
async def list_station_recipes(
|
async def list_station_recipes(
|
||||||
db: AsyncSession, station_id: int,
|
db: AsyncSession, station_id: int,
|
||||||
) -> list[Recipe]:
|
) -> list[Recipe]:
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
"""Integration tests for /api/production-runs.
|
||||||
|
|
||||||
|
The point of these tables is that a production outlives the page it was started
|
||||||
|
from, so the tests lean on what must survive: the deadline, the cycle count, the
|
||||||
|
trace of what happened, and the rule that a station has one production at a time.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from src.backend.models.orm.production import ProductionRun
|
||||||
|
from src.backend.models.orm.recipe import Recipe
|
||||||
|
from src.backend.models.orm.station import Station
|
||||||
|
from src.backend.services import auth_service, production_service
|
||||||
|
from src.backend.tests.conftest import auth_headers, create_test_recipe
|
||||||
|
|
||||||
|
|
||||||
|
async def _station(db_session, user_id: int, code: str = "ST-PROD") -> Station:
|
||||||
|
station = Station(code=code, name=f"Stazione {code}", active=True, created_by=user_id)
|
||||||
|
db_session.add(station)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(station)
|
||||||
|
return station
|
||||||
|
|
||||||
|
|
||||||
|
async def _recipe_with_interval(db_session, user_id: int, minutes, code="REC-PROD") -> Recipe:
|
||||||
|
recipe = await create_test_recipe(db_session, user_id=user_id, code=code)
|
||||||
|
recipe.measurement_interval_minutes = minutes
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(recipe)
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
async def _supervisor(db_session, username="capoturno", password="TurnoPwd1"):
|
||||||
|
user = await auth_service.create_user(
|
||||||
|
db_session,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
display_name="Capo Turno",
|
||||||
|
roles=["Supervisor"],
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
return user, password
|
||||||
|
|
||||||
|
|
||||||
|
async def _advance_clock(db_session, run_id: int, delta: timedelta) -> None:
|
||||||
|
"""Simulate wall-clock time passing, without sleeping in a test.
|
||||||
|
|
||||||
|
Winding every instant stored on the run back by D is indistinguishable from D
|
||||||
|
having elapsed. Moving only one of them would not be time passing - it would be
|
||||||
|
rewriting history, and would measure the wrong thing.
|
||||||
|
"""
|
||||||
|
run = (await db_session.execute(
|
||||||
|
select(ProductionRun).where(ProductionRun.id == run_id)
|
||||||
|
)).scalar_one()
|
||||||
|
run.started_at = run.started_at - delta
|
||||||
|
if run.next_measurement_at is not None:
|
||||||
|
run.next_measurement_at = run.next_measurement_at - delta
|
||||||
|
if run.paused_at is not None:
|
||||||
|
run.paused_at = run.paused_at - delta
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _open(client, user, station, recipe, **extra):
|
||||||
|
return await client.post(
|
||||||
|
"/api/production-runs",
|
||||||
|
headers=auth_headers(user),
|
||||||
|
json={"station_code": station.code, "recipe_id": recipe.id, **extra},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Opening
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_open_run_requires_auth(client: AsyncClient):
|
||||||
|
resp = await client.post("/api/production-runs", json={"station_code": "X", "recipe_id": 1})
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_open_run_starts_the_countdown(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id)
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, minutes=30)
|
||||||
|
|
||||||
|
resp = await _open(client, measurement_tec_user, station, recipe)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["status"] == "running"
|
||||||
|
assert body["cycle_count"] == 0
|
||||||
|
assert body["measurement_interval_minutes"] == 30
|
||||||
|
# About half an hour out, allowing for the round trip.
|
||||||
|
assert 29 * 60 <= body["seconds_to_next_measurement"] <= 30 * 60
|
||||||
|
assert body["overdue"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_open_run_without_interval_has_no_deadline(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-NOINT")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, None, code="REC-NOINT")
|
||||||
|
|
||||||
|
body = (await _open(client, measurement_tec_user, station, recipe)).json()
|
||||||
|
assert body["next_measurement_at"] is None
|
||||||
|
assert body["seconds_to_next_measurement"] is None
|
||||||
|
assert body["overdue"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_open_run_snapshots_the_interval(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""Editing the recipe mid-production must not move a running deadline."""
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-SNAP")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 20, code="REC-SNAP")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
|
||||||
|
recipe.measurement_interval_minutes = 90
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/api/production-runs/{run_id}", headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert resp.json()["measurement_interval_minutes"] == 20
|
||||||
|
|
||||||
|
|
||||||
|
async def test_station_cannot_have_two_open_runs(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-ONE")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-ONE")
|
||||||
|
first = await _open(client, measurement_tec_user, station, recipe)
|
||||||
|
assert first.status_code == 201
|
||||||
|
|
||||||
|
second = await _open(client, measurement_tec_user, station, recipe)
|
||||||
|
assert second.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
async def test_open_run_rejects_unknown_station(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-NOSTAT")
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/production-runs",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={"station_code": "ST-GHOST", "recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Reading the current run - what replaces the in-page state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_current_run_is_null_when_nothing_is_running(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-IDLE")
|
||||||
|
resp = await client.get(
|
||||||
|
"/api/production-runs/current",
|
||||||
|
params={"station_code": station.code},
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_current_run_survives_and_keeps_counting_down(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""The deadline is stored, so a fresh page load rejoins the same countdown."""
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-KEEP")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 45, code="REC-KEEP")
|
||||||
|
opened = (await _open(client, measurement_tec_user, station, recipe)).json()
|
||||||
|
|
||||||
|
later = await client.get(
|
||||||
|
"/api/production-runs/current",
|
||||||
|
params={"station_code": station.code},
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
body = later.json()
|
||||||
|
assert body["id"] == opened["id"]
|
||||||
|
assert body["next_measurement_at"] == opened["next_measurement_at"]
|
||||||
|
assert body["seconds_to_next_measurement"] <= opened["seconds_to_next_measurement"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cycles
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cycle_restarts_the_interval_and_counts(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-CYC")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-CYC")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/cycle", headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["cycle_count"] == 1
|
||||||
|
assert 9 * 60 <= body["seconds_to_next_measurement"] <= 10 * 60
|
||||||
|
|
||||||
|
|
||||||
|
async def test_overdue_run_reports_negative_seconds(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""Past the interval the countdown keeps going the other way, so the delay shows."""
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-LATE")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 5, code="REC-LATE")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
|
||||||
|
await _advance_clock(db_session, run_id, timedelta(minutes=8))
|
||||||
|
|
||||||
|
body = (await client.get(
|
||||||
|
"/api/production-runs/current",
|
||||||
|
params={"station_code": station.code},
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
)).json()
|
||||||
|
assert body["overdue"] is True
|
||||||
|
assert -190 <= body["seconds_to_next_measurement"] <= -170
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fermo linea / ripresa / chiusura
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pause_requires_supervisor_credentials(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-PAUSE1")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-PAUSE1")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={"supervisor_username": "nobody", "supervisor_password": "wrong"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pause_rejects_non_supervisor(
|
||||||
|
client: AsyncClient, measurement_tec_user, maker_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-PAUSE2")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-PAUSE2")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={
|
||||||
|
"supervisor_username": maker_user.username,
|
||||||
|
"supervisor_password": "testpassword123",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code in (401, 403)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pause_freezes_the_countdown(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""A fermo linea must not eat the measurement interval."""
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-FREEZE")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 30, code="REC-FREEZE")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-freeze")
|
||||||
|
|
||||||
|
paused = (await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={"supervisor_username": supervisor.username, "supervisor_password": password},
|
||||||
|
)).json()
|
||||||
|
assert paused["status"] == "paused"
|
||||||
|
frozen = paused["seconds_to_next_measurement"]
|
||||||
|
|
||||||
|
# An hour goes by with the line stopped: were the countdown still ticking, the
|
||||||
|
# run would now be deeply overdue.
|
||||||
|
await _advance_clock(db_session, run_id, timedelta(hours=1))
|
||||||
|
|
||||||
|
still = (await client.get(
|
||||||
|
"/api/production-runs/current",
|
||||||
|
params={"station_code": station.code},
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
)).json()
|
||||||
|
assert still["status"] == "paused"
|
||||||
|
assert still["seconds_to_next_measurement"] == frozen
|
||||||
|
assert still["overdue"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_resume_gives_back_the_time_the_stop_took(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-RESUME")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 30, code="REC-RESUME")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-resume")
|
||||||
|
creds = {"supervisor_username": supervisor.username, "supervisor_password": password}
|
||||||
|
|
||||||
|
frozen = (await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user), json=creds,
|
||||||
|
)).json()["seconds_to_next_measurement"]
|
||||||
|
|
||||||
|
# The line stays down for ten minutes.
|
||||||
|
await _advance_clock(db_session, run_id, timedelta(minutes=10))
|
||||||
|
|
||||||
|
resumed = (await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/resume",
|
||||||
|
headers=auth_headers(measurement_tec_user), json=creds,
|
||||||
|
)).json()
|
||||||
|
assert resumed["status"] == "running"
|
||||||
|
# The ten minutes of stop were handed back, not charged to the operator.
|
||||||
|
assert abs(resumed["seconds_to_next_measurement"] - frozen) <= 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cycle_refused_while_paused(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-PCYC")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-PCYC")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-pcyc")
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={"supervisor_username": supervisor.username, "supervisor_password": password},
|
||||||
|
)
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/cycle", headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
async def test_close_stops_the_timer_and_frees_the_station(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-CLOSE")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-CLOSE")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-close")
|
||||||
|
|
||||||
|
closed = (await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/close",
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
json={"supervisor_username": supervisor.username, "supervisor_password": password},
|
||||||
|
)).json()
|
||||||
|
assert closed["status"] == "closed"
|
||||||
|
assert closed["closed_by"] == supervisor.id
|
||||||
|
assert closed["seconds_to_next_measurement"] is None
|
||||||
|
|
||||||
|
# Nothing is open at the station any more...
|
||||||
|
current = await client.get(
|
||||||
|
"/api/production-runs/current",
|
||||||
|
params={"station_code": station.code},
|
||||||
|
headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert current.json() is None
|
||||||
|
|
||||||
|
# ...and a new production can start there.
|
||||||
|
again = await _open(client, measurement_tec_user, station, recipe)
|
||||||
|
assert again.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
async def test_closed_run_refuses_further_actions(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-DEAD")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-DEAD")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-dead")
|
||||||
|
creds = {"supervisor_username": supervisor.username, "supervisor_password": password}
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/close",
|
||||||
|
headers=auth_headers(measurement_tec_user), json=creds,
|
||||||
|
)
|
||||||
|
cycle = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/cycle", headers=auth_headers(measurement_tec_user),
|
||||||
|
)
|
||||||
|
assert cycle.status_code == 409
|
||||||
|
pause = await client.post(
|
||||||
|
f"/api/production-runs/{run_id}/pause",
|
||||||
|
headers=auth_headers(measurement_tec_user), json=creds,
|
||||||
|
)
|
||||||
|
assert pause.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# The trace
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_records_what_happened(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""The history a production never had: start, cycles, stop, resume, close."""
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-TRACE")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-TRACE")
|
||||||
|
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||||
|
supervisor, password = await _supervisor(db_session, username="capo-trace")
|
||||||
|
creds = {"supervisor_username": supervisor.username, "supervisor_password": password}
|
||||||
|
headers = auth_headers(measurement_tec_user)
|
||||||
|
|
||||||
|
await client.post(f"/api/production-runs/{run_id}/cycle", headers=headers)
|
||||||
|
await client.post(f"/api/production-runs/{run_id}/pause", headers=headers, json=creds)
|
||||||
|
await client.post(f"/api/production-runs/{run_id}/resume", headers=headers, json=creds)
|
||||||
|
await client.post(f"/api/production-runs/{run_id}/close", headers=headers, json=creds)
|
||||||
|
|
||||||
|
events = (await client.get(
|
||||||
|
f"/api/production-runs/{run_id}", headers=headers,
|
||||||
|
)).json()["events"]
|
||||||
|
assert [e["event_type"] for e in events] == [
|
||||||
|
"start", "cycle_completed", "line_stop", "resume", "close",
|
||||||
|
]
|
||||||
|
# Who authorised what is on the record.
|
||||||
|
by_type = {e["event_type"]: e for e in events}
|
||||||
|
assert by_type["line_stop"]["supervisor_id"] == supervisor.id
|
||||||
|
assert by_type["close"]["supervisor_id"] == supervisor.id
|
||||||
|
assert by_type["cycle_completed"]["supervisor_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_traceability_travels_with_the_run(
|
||||||
|
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||||
|
):
|
||||||
|
station = await _station(db_session, admin_user.id, code="ST-LOT")
|
||||||
|
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-LOT")
|
||||||
|
body = (await _open(
|
||||||
|
client, measurement_tec_user, station, recipe,
|
||||||
|
lot_number="LOT-42", serial_number="SN-7",
|
||||||
|
)).json()
|
||||||
|
assert body["lot_number"] == "LOT-42"
|
||||||
|
assert body["serial_number"] == "SN-7"
|
||||||
@@ -201,3 +201,176 @@ async def test_duplicate_assignment_returns_409(
|
|||||||
json={"recipe_id": recipe.id},
|
json={"recipe_id": recipe.id},
|
||||||
)
|
)
|
||||||
assert second.status_code == 409
|
assert second.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Station list carries assigned recipes (point 7)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_stations_includes_assigned_recipes(
|
||||||
|
client: AsyncClient, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""The admin list shows which recipes a station handles, without a call per row."""
|
||||||
|
recipe = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-LIST")
|
||||||
|
await db_session.commit()
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-WITHREC", "name": "Con ricette"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
await client.post(
|
||||||
|
f"/api/stations/{sid}/recipes",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await client.get("/api/stations", headers=auth_headers(admin_user))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
station = next(s for s in resp.json() if s["id"] == sid)
|
||||||
|
assert [r["code"] for r in station["recipes"]] == ["REC-LIST"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_stations_empty_recipes_when_unassigned(
|
||||||
|
client: AsyncClient, admin_user,
|
||||||
|
):
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-NOREC", "name": "Senza ricette"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
resp = await client.get("/api/stations", headers=auth_headers(admin_user))
|
||||||
|
station = next(s for s in resp.json() if s["id"] == sid)
|
||||||
|
assert station["recipes"] == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_stations_hides_inactive_recipes(
|
||||||
|
client: AsyncClient, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""An assigned but deactivated recipe must not show: the operator won't see it."""
|
||||||
|
recipe = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-OFF")
|
||||||
|
await db_session.commit()
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-INACT", "name": "Con ricetta spenta"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
await client.post(
|
||||||
|
f"/api/stations/{sid}/recipes",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
|
||||||
|
recipe.active = False
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.get("/api/stations", headers=auth_headers(admin_user))
|
||||||
|
station = next(s for s in resp.json() if s["id"] == sid)
|
||||||
|
assert station["recipes"] == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-row station reset (point 7)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reset_station_removes_all_assignments(
|
||||||
|
client: AsyncClient, admin_user, db_session,
|
||||||
|
):
|
||||||
|
first = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-R1")
|
||||||
|
second = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-R2")
|
||||||
|
await db_session.commit()
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-RESET", "name": "Da azzerare"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
for recipe in (first, second):
|
||||||
|
await client.post(
|
||||||
|
f"/api/stations/{sid}/recipes",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await client.delete(
|
||||||
|
f"/api/stations/{sid}/recipes", headers=auth_headers(admin_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
assert resp.json() == {"station_id": sid, "removed": 2}
|
||||||
|
|
||||||
|
left = await client.get(
|
||||||
|
f"/api/stations/{sid}/recipes", headers=auth_headers(admin_user),
|
||||||
|
)
|
||||||
|
assert left.json() == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reset_station_keeps_the_station(
|
||||||
|
client: AsyncClient, admin_user, db_session,
|
||||||
|
):
|
||||||
|
"""Reset clears assignments only - the station stays and can be reassigned."""
|
||||||
|
recipe = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-KEEP")
|
||||||
|
await db_session.commit()
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-KEEP", "name": "Resta"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
await client.post(
|
||||||
|
f"/api/stations/{sid}/recipes",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
await client.delete(f"/api/stations/{sid}/recipes", headers=auth_headers(admin_user))
|
||||||
|
|
||||||
|
still_there = await client.get(
|
||||||
|
f"/api/stations/{sid}", headers=auth_headers(admin_user),
|
||||||
|
)
|
||||||
|
assert still_there.status_code == 200
|
||||||
|
|
||||||
|
reassigned = await client.post(
|
||||||
|
f"/api/stations/{sid}/recipes",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"recipe_id": recipe.id},
|
||||||
|
)
|
||||||
|
assert reassigned.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reset_empty_station_is_idempotent(client: AsyncClient, admin_user):
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-IDEM", "name": "Gia vuota"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
resp = await client.delete(
|
||||||
|
f"/api/stations/{sid}/recipes", headers=auth_headers(admin_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["removed"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reset_station_non_admin_is_403(
|
||||||
|
client: AsyncClient, admin_user, maker_user,
|
||||||
|
):
|
||||||
|
created = await client.post(
|
||||||
|
"/api/stations",
|
||||||
|
headers=auth_headers(admin_user),
|
||||||
|
json={"code": "ST-RBAC", "name": "Protetta"},
|
||||||
|
)
|
||||||
|
sid = created.json()["id"]
|
||||||
|
resp = await client.delete(
|
||||||
|
f"/api/stations/{sid}/recipes", headers=auth_headers(maker_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reset_unknown_station_is_404(client: AsyncClient, admin_user):
|
||||||
|
resp = await client.delete(
|
||||||
|
"/api/stations/999999/recipes", headers=auth_headers(admin_user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|||||||
@@ -220,3 +220,14 @@ def api_unassign_recipe(station_id: int, recipe_id: int):
|
|||||||
if isinstance(resp, dict) and resp.get("error"):
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
return jsonify(resp), resp.get("status_code", 500)
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
return jsonify({"deleted": True}), 200
|
return jsonify({"deleted": True}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/api/stations/<int:station_id>/recipes", methods=["DELETE"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_reset_station_recipes(station_id: int):
|
||||||
|
"""Proxy: Clear every recipe assignment of a station."""
|
||||||
|
resp = api_client.delete(f"/api/stations/{station_id}/recipes")
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|||||||
@@ -12,6 +12,61 @@ from services.file_proxy import proxy_file
|
|||||||
|
|
||||||
measure_bp = Blueprint("measure", __name__)
|
measure_bp = Blueprint("measure", __name__)
|
||||||
|
|
||||||
|
# Session key holding a station chosen at runtime, overriding Config.STATION_CODE.
|
||||||
|
STATION_OVERRIDE_KEY = "station_override"
|
||||||
|
|
||||||
|
|
||||||
|
def _current_station() -> tuple[str | None, bool]:
|
||||||
|
"""Return the station this client acts as, and whether it is an override."""
|
||||||
|
override = session.get(STATION_OVERRIDE_KEY)
|
||||||
|
if override:
|
||||||
|
return override, True
|
||||||
|
return Config.STATION_CODE, False
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_station_switch() -> None:
|
||||||
|
"""Honour ?station=CODE, used to exercise several stations from one PC.
|
||||||
|
|
||||||
|
Only active when STATION_SWITCH_ENABLED is set: in production the station
|
||||||
|
identity comes from the local install, and silently measuring against another
|
||||||
|
station's recipes would break traceability. An empty value clears the override
|
||||||
|
and falls back to the configured station.
|
||||||
|
|
||||||
|
The requested code is validated against the server before being stored, so a
|
||||||
|
typo leaves the operator on the current station with an explanation rather
|
||||||
|
than stranded on a station that does not exist.
|
||||||
|
"""
|
||||||
|
if "station" not in request.args:
|
||||||
|
return
|
||||||
|
|
||||||
|
requested = (request.args.get("station") or "").strip().upper()
|
||||||
|
|
||||||
|
if not Config.STATION_SWITCH_ENABLED:
|
||||||
|
flash(_("Il cambio stazione da URL non è abilitato su questa postazione."), "error")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not requested:
|
||||||
|
if session.pop(STATION_OVERRIDE_KEY, None):
|
||||||
|
flash(_("Stazione riportata a quella configurata."), "info")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
probe = api_client.get_station_recipes(requested)
|
||||||
|
except Exception:
|
||||||
|
flash(_("Impossibile contattare il server per cambiare stazione."), "error")
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(probe, dict) and probe.get("error"):
|
||||||
|
flash(
|
||||||
|
_("Stazione '%(code)s' inesistente o disattivata: stazione invariata.",
|
||||||
|
code=requested),
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
session[STATION_OVERRIDE_KEY] = requested
|
||||||
|
flash(_("Stazione corrente: %(code)s", code=requested), "info")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Route: Recipe selection
|
# Route: Recipe selection
|
||||||
@@ -21,13 +76,16 @@ measure_bp = Blueprint("measure", __name__)
|
|||||||
@role_required("MeasurementTec")
|
@role_required("MeasurementTec")
|
||||||
def select_recipe():
|
def select_recipe():
|
||||||
"""Recipe selection page with search and barcode support."""
|
"""Recipe selection page with search and barcode support."""
|
||||||
# Fail-fast if STATION_CODE is not configured
|
_apply_station_switch()
|
||||||
if not Config.STATION_CODE:
|
station_code, station_overridden = _current_station()
|
||||||
|
|
||||||
|
# Fail-fast if no station is configured and none was chosen
|
||||||
|
if not station_code:
|
||||||
return render_template("errors/station_not_configured.html"), 503
|
return render_template("errors/station_not_configured.html"), 503
|
||||||
|
|
||||||
# Load recipes filtered by station
|
# Load recipes filtered by station
|
||||||
try:
|
try:
|
||||||
resp = api_client.get_station_recipes(Config.STATION_CODE)
|
resp = api_client.get_station_recipes(station_code)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return render_template(
|
return render_template(
|
||||||
"errors/station_not_configured.html", error=str(e),
|
"errors/station_not_configured.html", error=str(e),
|
||||||
@@ -52,7 +110,9 @@ def select_recipe():
|
|||||||
return render_template(
|
return render_template(
|
||||||
"measure/select_recipe.html",
|
"measure/select_recipe.html",
|
||||||
recipes=recipes,
|
recipes=recipes,
|
||||||
station_code=Config.STATION_CODE,
|
station_code=station_code,
|
||||||
|
station_overridden=station_overridden,
|
||||||
|
station_configured=Config.STATION_CODE,
|
||||||
auto_recipe_code=auto_recipe_code,
|
auto_recipe_code=auto_recipe_code,
|
||||||
auto_lot=auto_lot,
|
auto_lot=auto_lot,
|
||||||
auto_serial=auto_serial,
|
auto_serial=auto_serial,
|
||||||
@@ -354,6 +414,67 @@ def validate_supervisor():
|
|||||||
return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200
|
return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routes: Production run (state that must outlive the page)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@measure_bp.route("/api/production/current", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@role_required("MeasurementTec")
|
||||||
|
def api_current_production():
|
||||||
|
"""Proxy: the production open at this station, or null.
|
||||||
|
|
||||||
|
Every page asks this on load. Before, the timer and the cycle count lived in the
|
||||||
|
Alpine component and a change of task - a full page load - wiped them.
|
||||||
|
"""
|
||||||
|
station_code, _overridden = _current_station()
|
||||||
|
if not station_code:
|
||||||
|
return jsonify({"error": True, "detail": _("Stazione non configurata")}), 503
|
||||||
|
|
||||||
|
resp = api_client.get(
|
||||||
|
"/api/production-runs/current", params={"station_code": station_code},
|
||||||
|
)
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
|
@measure_bp.route("/api/production/start", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@role_required("MeasurementTec")
|
||||||
|
def api_start_production():
|
||||||
|
"""Proxy: open a production at this station."""
|
||||||
|
station_code, _overridden = _current_station()
|
||||||
|
if not station_code:
|
||||||
|
return jsonify({"error": True, "detail": _("Stazione non configurata")}), 503
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
payload = {
|
||||||
|
"station_code": station_code,
|
||||||
|
"recipe_id": data.get("recipe_id"),
|
||||||
|
"version_id": data.get("version_id"),
|
||||||
|
"lot_number": data.get("lot_number") or session.get("lot_number") or None,
|
||||||
|
"serial_number": data.get("serial_number") or session.get("serial_number") or None,
|
||||||
|
}
|
||||||
|
resp = api_client.post("/api/production-runs", data=payload)
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
return jsonify(resp), 201
|
||||||
|
|
||||||
|
|
||||||
|
@measure_bp.route("/api/production/<int:run_id>/cycle", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@role_required("MeasurementTec")
|
||||||
|
def api_complete_cycle(run_id: int):
|
||||||
|
"""Proxy: record a finished measurement cycle and restart the interval."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
resp = api_client.post(
|
||||||
|
f"/api/production-runs/{run_id}/cycle", data={"note": data.get("note")},
|
||||||
|
)
|
||||||
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Route: File proxy (browser can't send X-API-Key directly)
|
# Route: File proxy (browser can't send X-API-Key directly)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ class Config:
|
|||||||
# code it belongs to. Empty/None means "not configured".
|
# code it belongs to. Empty/None means "not configured".
|
||||||
STATION_CODE: str | None = os.getenv("STATION_CODE") or None
|
STATION_CODE: str | None = os.getenv("STATION_CODE") or None
|
||||||
|
|
||||||
|
# Allow switching the current station from the URL (?station=CODE), keeping the
|
||||||
|
# override in the session. Meant for commissioning: it lets one PC exercise
|
||||||
|
# several stations instead of needing one machine per station.
|
||||||
|
# Off by default - on the shop floor the station identity comes from the local
|
||||||
|
# install, and measuring against the wrong station's recipes would silently
|
||||||
|
# break traceability.
|
||||||
|
STATION_SWITCH_ENABLED: bool = os.getenv("STATION_SWITCH_ENABLED", "0") == "1"
|
||||||
|
|
||||||
# Babel i18n
|
# Babel i18n
|
||||||
BABEL_DEFAULT_LOCALE = "it"
|
BABEL_DEFAULT_LOCALE = "it"
|
||||||
BABEL_DEFAULT_TIMEZONE = "Europe/Rome"
|
BABEL_DEFAULT_TIMEZONE = "Europe/Rome"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Librerie di terze parti — copie locali
|
||||||
|
|
||||||
|
Queste copie servono al **punto 12** del documento delle modifiche: l'installazione a
|
||||||
|
Tràfilo è on-premise, su rete di produzione isolata. Finché queste librerie venivano
|
||||||
|
scaricate da CDN a ogni apertura di pagina, senza internet l'interfaccia non partiva
|
||||||
|
affatto (Alpine.js governa tutta la UI).
|
||||||
|
|
||||||
|
Secondo effetto, non meno importante: le versioni sono **congelate**. Prima Alpine.js
|
||||||
|
era agganciato a `alpinejs@3.x.x`, cioè l'applicazione cambiava da sola a ogni rilascio
|
||||||
|
degli autori, senza validazione. Per un sistema che produce evidenze per audit
|
||||||
|
ISO 9001 / IATF 16949 questo è di per sé un difetto.
|
||||||
|
|
||||||
|
Scaricate il **28 luglio 2026**.
|
||||||
|
|
||||||
|
## JavaScript
|
||||||
|
|
||||||
|
| File | Versione | Origine |
|
||||||
|
|---|---|---|
|
||||||
|
| `alpinejs-3.15.12.min.js` | 3.15.12 | `https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js` |
|
||||||
|
| `plotly-2.32.0.min.js` | 2.32.0 | `https://cdn.plot.ly/plotly-2.32.0.min.js` |
|
||||||
|
| `pdf-3.11.174.min.js` | 3.11.174 | `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` |
|
||||||
|
| `pdf.worker-3.11.174.min.js` | 3.11.174 | `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js` |
|
||||||
|
| `fabric-5.3.1.min.js` | 5.3.1 | `https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js` |
|
||||||
|
|
||||||
|
La versione di Alpine è quella a cui `3.x.x` risolveva il giorno del congelamento:
|
||||||
|
il pin non cambia il comportamento rispetto a quanto già in esercizio.
|
||||||
|
|
||||||
|
`pdf.worker` è un file a sé che PDF.js carica **a runtime** via
|
||||||
|
`pdfjsLib.GlobalWorkerOptions.workerSrc`. Va ripuntato in tutti i template che usano
|
||||||
|
PDF.js, non basta lo script principale: altrimenti la libreria si carica in locale e il
|
||||||
|
worker continua a cercare internet — sembra funzionare finché non si apre un disegno.
|
||||||
|
|
||||||
|
## Font
|
||||||
|
|
||||||
|
`fonts.css` sostituisce il `<link>` a `fonts.googleapis.com` in `base.html`.
|
||||||
|
|
||||||
|
| File | Famiglia | Subset |
|
||||||
|
|---|---|---|
|
||||||
|
| `fonts/inter-latin.woff2` | Inter | latin |
|
||||||
|
| `fonts/inter-latin-ext.woff2` | Inter | latin-ext |
|
||||||
|
| `fonts/jetbrains-mono-latin.woff2` | JetBrains Mono | latin |
|
||||||
|
| `fonts/jetbrains-mono-latin-ext.woff2` | JetBrains Mono | latin-ext |
|
||||||
|
|
||||||
|
Entrambe le famiglie sono **font variabili** (asse `wght`: Inter 100–900, JetBrains Mono
|
||||||
|
400–800): un solo file per subset copre tutti i pesi usati dall'interfaccia. Google
|
||||||
|
serviva lo stesso woff2 sotto otto URL diversi, uno per peso dichiarato.
|
||||||
|
|
||||||
|
Tenuti i soli subset **latin** e **latin-ext**, che coprono per intero italiano e
|
||||||
|
inglese. Per gli altri alfabeti il browser ricade sul font di sistema — scelta
|
||||||
|
consapevole: cirillico, greco e vietnamita valevano 600 KB di peso morto.
|
||||||
|
|
||||||
|
## Impronte SHA-256
|
||||||
|
|
||||||
|
```
|
||||||
|
57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f alpinejs-3.15.12.min.js
|
||||||
|
bb0442d69a4d4673320f689bc6e16c05a0e3f16ccfbf551f394d90a7e7cfc772 fabric-5.3.1.min.js
|
||||||
|
5b5799e6f8c680663207ac5b42ee14eed2a406fa7af48f50c154f0c0b1566946 pdf-3.11.174.min.js
|
||||||
|
feabdf309770ed24bba31a5467836cdc8cf639c705af27d52b585b041bb8527b pdf.worker-3.11.174.min.js
|
||||||
|
0a17719a72751704861215da0e5c5cdb3f9a8d50eff5cb84cb6f8b80786682b0 plotly-2.32.0.min.js
|
||||||
|
34b9c504cab7a73e37b746343a449132e56cf7b5481af2cb81dc74dcff25c956 fonts/inter-latin-ext.woff2
|
||||||
|
3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62 fonts/inter-latin.woff2
|
||||||
|
db5ff4db83e580426280e9337a58dc57d3a83784a1b03ad80914651594441d52 fonts/jetbrains-mono-latin-ext.woff2
|
||||||
|
83c005d49d8a6a50474c73a5a36ac0468076e9c4a29da7bdb14995d80560a5be fonts/jetbrains-mono-latin.woff2
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifica: `sha256sum -c` da questa cartella, oppure
|
||||||
|
`sha256sum *.js fonts/*.woff2`.
|
||||||
|
|
||||||
|
## Aggiornare una libreria
|
||||||
|
|
||||||
|
1. Scaricare la nuova versione con il nome comprensivo di versione.
|
||||||
|
2. Ripuntare i `<script>`/`<link>` nei template (`grep -rn vendor/ ../../templates/`).
|
||||||
|
3. Aggiornare questa tabella e le impronte.
|
||||||
|
4. Rimuovere il file vecchio.
|
||||||
|
|
||||||
|
Il nome del file porta la versione apposta: un aggiornamento non passa inosservato in
|
||||||
|
revisione, e la modifica resta tracciata in git.
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/* Inter + JetBrains Mono self-hosted — subset latin + latin-ext, font variabili.
|
||||||
|
Sostituisce il <link> a fonts.googleapis.com: l'app deve partire a rete staccata
|
||||||
|
(punto 12). Un solo woff2 per subset copre tutti i pesi via asse wght.
|
||||||
|
Provenienza e hash dei file: vendor/VERSIONS.md */
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(fonts/inter-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(fonts/inter-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(fonts/jetbrains-mono-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(fonts/jetbrains-mono-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -49,6 +49,7 @@
|
|||||||
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Codice') }}</th>
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Codice') }}</th>
|
||||||
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Nome') }}</th>
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Nome') }}</th>
|
||||||
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider hidden md:table-cell">{{ _('Postazione') }}</th>
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider hidden md:table-cell">{{ _('Postazione') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider hidden lg:table-cell">{{ _('Ricette collegate') }}</th>
|
||||||
<th class="text-center px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Stato') }}</th>
|
<th class="text-center px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Stato') }}</th>
|
||||||
<th class="text-right px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Azioni') }}</th>
|
<th class="text-right px-4 py-3 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Azioni') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -62,6 +63,24 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-[var(--text-primary)]" x-text="station.name"></td>
|
<td class="px-4 py-3 text-sm text-[var(--text-primary)]" x-text="station.name"></td>
|
||||||
<td class="px-4 py-3 text-sm text-[var(--text-secondary)] hidden md:table-cell" x-text="station.location || '-'"></td>
|
<td class="px-4 py-3 text-sm text-[var(--text-secondary)] hidden md:table-cell" x-text="station.location || '-'"></td>
|
||||||
|
<td class="px-4 py-3 hidden lg:table-cell">
|
||||||
|
<template x-if="!(station.recipes && station.recipes.length)">
|
||||||
|
<span class="text-xs italic text-[var(--text-secondary)]">{{ _('Nessuna ricetta') }}</span>
|
||||||
|
</template>
|
||||||
|
<template x-if="station.recipes && station.recipes.length">
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
<template x-for="r in station.recipes.slice(0, 3)" :key="r.id">
|
||||||
|
<span class="inline-flex px-1.5 py-0.5 rounded font-mono text-[11px] font-medium
|
||||||
|
bg-primary-50 dark:bg-primary-900/20 text-primary"
|
||||||
|
:title="r.name" x-text="r.code"></span>
|
||||||
|
</template>
|
||||||
|
<template x-if="station.recipes.length > 3">
|
||||||
|
<span class="text-[11px] text-[var(--text-secondary)]"
|
||||||
|
x-text="'+' + (station.recipes.length - 3)"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium"
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium"
|
||||||
:class="station.active
|
:class="station.active
|
||||||
@@ -78,6 +97,17 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="confirmReset(station)"
|
||||||
|
:disabled="!(station.recipes && station.recipes.length)"
|
||||||
|
class="p-1.5 rounded-lg text-[var(--text-secondary)] hover:text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors
|
||||||
|
disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:text-[var(--text-secondary)] disabled:hover:bg-transparent"
|
||||||
|
:title="(station.recipes && station.recipes.length)
|
||||||
|
? '{{ _('Azzera le ricette della stazione') }}'
|
||||||
|
: '{{ _('Nessuna ricetta da azzerare') }}'">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992V4.356m-.582 9.66a8.25 8.25 0 01-15.356 2.008M3.985 14.652h4.992v4.992m.581-9.66a8.25 8.25 0 0115.356-2.008"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button @click="openEditModal(station)"
|
<button @click="openEditModal(station)"
|
||||||
class="p-1.5 rounded-lg text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
|
class="p-1.5 rounded-lg text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
|
||||||
:title="'{{ _('Modifica') }}'">
|
:title="'{{ _('Modifica') }}'">
|
||||||
@@ -335,6 +365,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Reset Modal -->
|
||||||
|
<div x-show="showResetConfirm" x-cloak
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
@keydown.escape.window="showResetConfirm = false">
|
||||||
|
<div class="absolute inset-0 bg-black/50" @click="showResetConfirm = false"></div>
|
||||||
|
<div x-show="showResetConfirm"
|
||||||
|
x-transition
|
||||||
|
class="relative bg-[var(--bg-card)] rounded-xl border border-[var(--border-color)] shadow-xl w-full max-w-sm p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-2">{{ _('Azzera Stazione') }}</h3>
|
||||||
|
<p class="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
{{ _('Rimuovere tutte le ricette assegnate a') }}
|
||||||
|
<strong x-text="resetTarget?.code"></strong>?
|
||||||
|
<br>
|
||||||
|
<span class="text-xs text-[var(--text-secondary)]">
|
||||||
|
<span x-text="resetTarget?.recipes?.length || 0"></span>
|
||||||
|
{{ _('assegnazioni verranno rimosse. La stazione resta, e si potrà riassegnare.') }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showResetConfirm = false"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-secondary)] transition-colors">
|
||||||
|
{{ _('Annulla') }}
|
||||||
|
</button>
|
||||||
|
<button @click="performReset()"
|
||||||
|
:disabled="saving"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white rounded-lg shadow-sm bg-amber-600 hover:bg-amber-700 disabled:opacity-50 transition-colors">
|
||||||
|
{{ _('Azzera') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Delete Modal -->
|
<!-- Confirm Delete Modal -->
|
||||||
<div x-show="showDeleteConfirm" x-cloak
|
<div x-show="showDeleteConfirm" x-cloak
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
@@ -375,11 +437,13 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
showModal: false,
|
showModal: false,
|
||||||
showAssignments: false,
|
showAssignments: false,
|
||||||
showDeleteConfirm: false,
|
showDeleteConfirm: false,
|
||||||
|
showResetConfirm: false,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
editingId: null,
|
editingId: null,
|
||||||
saving: false,
|
saving: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
deleteTarget: null,
|
deleteTarget: null,
|
||||||
|
resetTarget: null,
|
||||||
assignmentStation: null,
|
assignmentStation: null,
|
||||||
assignedRecipes: [],
|
assignedRecipes: [],
|
||||||
recipeSearch: '',
|
recipeSearch: '',
|
||||||
@@ -486,7 +550,9 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const idx = this.stations.findIndex(s => s.id === this.editingId);
|
const idx = this.stations.findIndex(s => s.id === this.editingId);
|
||||||
if (idx >= 0) this.stations[idx] = result;
|
// PUT returns the station without its assignments; keep the ones the
|
||||||
|
// row already knows about or the "ricette collegate" column blanks out.
|
||||||
|
if (idx >= 0) this.stations[idx] = { ...result, recipes: this.stations[idx].recipes || [] };
|
||||||
} else {
|
} else {
|
||||||
if (!this.form.code || !this.form.name) {
|
if (!this.form.code || !this.form.name) {
|
||||||
this.errorMsg = '{{ _("Codice e nome sono obbligatori") }}';
|
this.errorMsg = '{{ _("Codice e nome sono obbligatori") }}';
|
||||||
@@ -509,7 +575,7 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
this.errorMsg = result.detail || '{{ _("Errore nella creazione") }}';
|
this.errorMsg = result.detail || '{{ _("Errore nella creazione") }}';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.stations.push(result);
|
this.stations.push({ ...result, recipes: [] });
|
||||||
}
|
}
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -524,6 +590,40 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
this.showDeleteConfirm = true;
|
this.showDeleteConfirm = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
confirmReset(station) {
|
||||||
|
if (!station.recipes || station.recipes.length === 0) return;
|
||||||
|
this.resetTarget = station;
|
||||||
|
this.showResetConfirm = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async performReset() {
|
||||||
|
if (!this.resetTarget) return;
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/admin/api/stations/${this.resetTarget.id}/recipes`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'X-CSRFToken': this.csrfToken },
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const result = await resp.json().catch(() => ({}));
|
||||||
|
alert(result.detail || '{{ _("Errore nell azzeramento") }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// resetTarget is the same object the row renders, so clearing it here
|
||||||
|
// updates the chips column without reloading the page.
|
||||||
|
this.resetTarget.recipes = [];
|
||||||
|
if (this.assignmentStation && this.assignmentStation.id === this.resetTarget.id) {
|
||||||
|
this.assignedRecipes = [];
|
||||||
|
}
|
||||||
|
this.showResetConfirm = false;
|
||||||
|
this.resetTarget = null;
|
||||||
|
} catch (e) {
|
||||||
|
alert('{{ _("Errore di connessione al server") }}');
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async performDelete() {
|
async performDelete() {
|
||||||
if (!this.deleteTarget) return;
|
if (!this.deleteTarget) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
@@ -591,7 +691,11 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const recipe = this.allRecipes.find(r => r.id === id);
|
const recipe = this.allRecipes.find(r => r.id === id);
|
||||||
if (recipe) this.assignedRecipes.push({ id: recipe.id, code: recipe.code, name: recipe.name, active: recipe.active });
|
if (recipe) {
|
||||||
|
const summary = { id: recipe.id, code: recipe.code, name: recipe.name, active: recipe.active };
|
||||||
|
this.assignedRecipes.push(summary);
|
||||||
|
this._syncStationRecipes();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -614,12 +718,23 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.assignedRecipes = this.assignedRecipes.filter(r => r.id !== recipeId);
|
this.assignedRecipes = this.assignedRecipes.filter(r => r.id !== recipeId);
|
||||||
|
this._syncStationRecipes();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
||||||
} finally {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* Mirror the modal's assignment list onto the table row, so the chips in
|
||||||
|
"ricette collegate" stay true after assigning or unassigning. Sorted by
|
||||||
|
code to match the order the server returns. */
|
||||||
|
_syncStationRecipes() {
|
||||||
|
if (!this.assignmentStation) return;
|
||||||
|
this.assignmentStation.recipes = [...this.assignedRecipes]
|
||||||
|
.filter(r => r.active !== false)
|
||||||
|
.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,10 +13,8 @@
|
|||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/tmflow-icon.svg') }}">
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/tmflow-icon.svg') }}">
|
||||||
|
|
||||||
<!-- Google Fonts: Inter + JetBrains Mono -->
|
<!-- Fonts: Inter + JetBrains Mono (copie locali, vedi static/vendor/VERSIONS.md) -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fonts.css') }}">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- TailwindCSS (compiled) -->
|
<!-- TailwindCSS (compiled) -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
|
||||||
@@ -186,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Alpine.js CDN (defer) - must load AFTER extra_js so component functions are defined -->
|
<!-- Alpine.js (defer) - must load AFTER extra_js so component functions are defined -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="{{ url_for('static', filename='vendor/alpinejs-3.15.12.min.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -432,9 +432,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -379,10 +379,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/fabric-5.3.1.min.js') }}"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=25"></script>
|
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=25"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1056,8 +1056,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';</script>
|
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
// Force reload when page is restored from browser bfcache (back/forward navigation).
|
// Force reload when page is restored from browser bfcache (back/forward navigation).
|
||||||
|
|||||||
@@ -78,6 +78,23 @@
|
|||||||
<p class="mt-1 text-sm text-steel-500 dark:text-steel-400">
|
<p class="mt-1 text-sm text-steel-500 dark:text-steel-400">
|
||||||
{{ _('Stazione') }}: <span class="font-mono font-bold">{{ station_code }}</span>
|
{{ _('Stazione') }}: <span class="font-mono font-bold">{{ station_code }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
{% if station_overridden %}
|
||||||
|
<p class="mt-2 inline-flex flex-wrap items-center gap-2 px-2.5 py-1 rounded-lg
|
||||||
|
bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700
|
||||||
|
text-xs text-amber-800 dark:text-amber-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{{ _('Stazione forzata per collaudo') }}{% if station_configured %} — {{ _('configurata') }}:
|
||||||
|
<span class="font-mono">{{ station_configured }}</span>{% endif %}
|
||||||
|
</span>
|
||||||
|
<a href="{{ url_for('measure.select_recipe', station='') }}"
|
||||||
|
class="font-medium underline underline-offset-2 hover:no-underline">
|
||||||
|
{{ _('Ripristina') }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -428,6 +428,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ================================================================
|
||||||
|
PRODUCTION ERROR — the server refused to record the production state.
|
||||||
|
Shown because the fallback keeps the operator working locally, and a timer
|
||||||
|
that is not backed by the server must never look like one that is.
|
||||||
|
================================================================ #}
|
||||||
|
<div x-show="productionError"
|
||||||
|
x-transition
|
||||||
|
x-cloak
|
||||||
|
class="shrink-0 bg-red-50 dark:bg-red-900/20 border-t border-red-300 dark:border-red-700 px-4 py-2">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<svg class="w-4 h-4 text-red-600 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-red-800 dark:text-red-200">
|
||||||
|
{{ _('Produzione non registrata sul server') }}:
|
||||||
|
<span x-text="productionError"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ================================================================
|
{# ================================================================
|
||||||
AVVIO PRODUZIONE — visible after first cycle, before production started
|
AVVIO PRODUZIONE — visible after first cycle, before production started
|
||||||
================================================================ #}
|
================================================================ #}
|
||||||
@@ -680,9 +700,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
@@ -709,6 +729,8 @@ function taskExecute() {
|
|||||||
showCompletionOverlay: false,
|
showCompletionOverlay: false,
|
||||||
|
|
||||||
// ---- Measurement timer ----
|
// ---- Measurement timer ----
|
||||||
|
// The interval is only used for display: the deadline itself lives on the server,
|
||||||
|
// so it survives the page. See loadProductionRun().
|
||||||
measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
|
measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
|
||||||
timerActive: false,
|
timerActive: false,
|
||||||
timerRemaining: 0,
|
timerRemaining: 0,
|
||||||
@@ -716,6 +738,12 @@ function taskExecute() {
|
|||||||
cycleCount: 0,
|
cycleCount: 0,
|
||||||
productionStarted: false,
|
productionStarted: false,
|
||||||
|
|
||||||
|
// ---- Production run (server-side state) ----
|
||||||
|
// Navigating between tasks is a full page load, so anything kept only here dies.
|
||||||
|
// This is read back from the server on every load instead.
|
||||||
|
productionRun: null,
|
||||||
|
productionError: '',
|
||||||
|
|
||||||
// ---- Cycle & workflow state ----
|
// ---- Cycle & workflow state ----
|
||||||
cycleConfirmed: false,
|
cycleConfirmed: false,
|
||||||
showSupervisorModal: false,
|
showSupervisorModal: false,
|
||||||
@@ -812,6 +840,39 @@ function taskExecute() {
|
|||||||
init() {
|
init() {
|
||||||
this.subtasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
this.subtasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
||||||
this.inputStartedAt = Date.now();
|
this.inputStartedAt = Date.now();
|
||||||
|
// Rejoin whatever production is already open at this station.
|
||||||
|
this.loadProductionRun();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Production run: read the state back from the server ----
|
||||||
|
|
||||||
|
async loadProductionRun() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('{{ url_for("measure.api_current_production") }}');
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const run = await resp.json();
|
||||||
|
if (run && run.id) this.adoptProductionRun(run);
|
||||||
|
} catch (e) {
|
||||||
|
// Offline or server down: leave the page usable, just without the timer.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Take the server's word for the state of the production. Called on load and
|
||||||
|
after every action that changes it, so the page never drifts from the truth. */
|
||||||
|
adoptProductionRun(run) {
|
||||||
|
this.productionRun = run;
|
||||||
|
this.productionStarted = run.status !== 'closed';
|
||||||
|
this.cycleCount = run.cycle_count;
|
||||||
|
|
||||||
|
const seconds = run.seconds_to_next_measurement;
|
||||||
|
if (seconds === null || seconds === undefined || run.status !== 'running') {
|
||||||
|
this.stopMeasurementTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The server hands over seconds already computed rather than a timestamp: a
|
||||||
|
// naive datetime would be read in the browser's timezone and the countdown
|
||||||
|
// would be off by the UTC offset.
|
||||||
|
this.startCountdownFrom(seconds);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Check if a subtask has been measured ----
|
// ---- Check if a subtask has been measured ----
|
||||||
@@ -953,21 +1014,57 @@ function taskExecute() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ---- Confirm measurement cycle (Fine ciclo misura) ----
|
// ---- Confirm measurement cycle (Fine ciclo misura) ----
|
||||||
confirmCycle() {
|
async confirmCycle() {
|
||||||
this.cycleConfirmed = true;
|
this.cycleConfirmed = true;
|
||||||
this.showCompletionOverlay = false;
|
this.showCompletionOverlay = false;
|
||||||
this.cycleCount++;
|
|
||||||
|
|
||||||
// Start measurement timer if recipe has an interval
|
// Recorded server-side so the count and the next deadline outlive this page.
|
||||||
|
if (this.productionRun) {
|
||||||
|
const run = await this.postProduction(
|
||||||
|
'{{ url_for("measure.api_complete_cycle", run_id=0) }}'.replace('/0/', '/' + this.productionRun.id + '/'),
|
||||||
|
);
|
||||||
|
if (run) {
|
||||||
|
this.adoptProductionRun(run);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No production open (recipe run outside a production): keep the old local
|
||||||
|
// behaviour rather than leaving the operator without a timer.
|
||||||
|
this.cycleCount++;
|
||||||
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
||||||
this.startMeasurementTimer();
|
this.startCountdownFrom(this.measurementIntervalMinutes * 60);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* POST to a production endpoint, returning the updated run or null on failure. */
|
||||||
|
async postProduction(url, body) {
|
||||||
|
this.productionError = '';
|
||||||
|
try {
|
||||||
|
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||||||
|
body: JSON.stringify(body || {}),
|
||||||
|
});
|
||||||
|
const data = await resp.json().catch(() => null);
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.productionError = (data && data.detail) || '{{ _("Errore di comunicazione con il server") }}';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
this.productionError = '{{ _("Errore di connessione") }}';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Measurement timer ----
|
// ---- Measurement timer ----
|
||||||
startMeasurementTimer() {
|
/* Ticks locally for a smooth display, but the number it starts from always comes
|
||||||
|
from the server, and every page load resynchronises it. */
|
||||||
|
startCountdownFrom(seconds) {
|
||||||
this.stopMeasurementTimer();
|
this.stopMeasurementTimer();
|
||||||
this.timerRemaining = this.measurementIntervalMinutes * 60;
|
this.timerRemaining = seconds;
|
||||||
this.timerActive = true;
|
this.timerActive = true;
|
||||||
var self = this;
|
var self = this;
|
||||||
this._timerInterval = setInterval(function () {
|
this._timerInterval = setInterval(function () {
|
||||||
@@ -1014,11 +1111,26 @@ function taskExecute() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Avvio Produzione (GAIA placeholder) ----
|
// ---- Avvio Produzione ----
|
||||||
startProduction() {
|
/* Opens a production run on the server. That row is what makes the timer, the
|
||||||
this.productionStarted = true;
|
cycle count and the history survive a change of task.
|
||||||
// TODO: integrazione GAIA — inviare segnale per avviare timer linea
|
The hand-off to the ERP (GAIA) plugs in on top of this, once the protocol is
|
||||||
// await fetch('/measure/api/gaia/start-production', { method: 'POST', ... });
|
agreed: everything before it works without waiting for that. */
|
||||||
|
async startProduction() {
|
||||||
|
const run = await this.postProduction(
|
||||||
|
'{{ url_for("measure.api_start_production") }}',
|
||||||
|
{
|
||||||
|
recipe_id: this.task.recipe_id,
|
||||||
|
version_id: this.task.version_id,
|
||||||
|
lot_number: this.lotNumber || null,
|
||||||
|
serial_number: this.serialNumber || null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (run) {
|
||||||
|
this.adoptProductionRun(run);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Is the current task the last one in the recipe sequence?
|
// Is the current task the last one in the recipe sequence?
|
||||||
@@ -1030,14 +1142,22 @@ function taskExecute() {
|
|||||||
|
|
||||||
// Start production from the completion overlay (last START task) and begin
|
// Start production from the completion overlay (last START task) and begin
|
||||||
// the measurement-interval cycle if the recipe defines one.
|
// the measurement-interval cycle if the recipe defines one.
|
||||||
startProductionFromOverlay() {
|
async startProductionFromOverlay() {
|
||||||
this.showCompletionOverlay = false;
|
this.showCompletionOverlay = false;
|
||||||
this.startProduction();
|
const opened = await this.startProduction();
|
||||||
|
if (!opened) {
|
||||||
|
// Server refused: keep the operator working rather than stranding them
|
||||||
|
// mid-shift, but productionError is on screen so it is not silent.
|
||||||
this.cycleConfirmed = true;
|
this.cycleConfirmed = true;
|
||||||
this.cycleCount++;
|
this.cycleCount++;
|
||||||
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
||||||
this.startMeasurementTimer();
|
this.startCountdownFrom(this.measurementIntervalMinutes * 60);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The START-phase measurements have just been taken: record them as the first
|
||||||
|
// cycle, which is also what starts the interval running.
|
||||||
|
await this.confirmCycle();
|
||||||
},
|
},
|
||||||
|
|
||||||
get timerDisplay() {
|
get timerDisplay() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<!-- Plotly.js CDN -->
|
<!-- Plotly.js CDN -->
|
||||||
<script src="https://cdn.plot.ly/plotly-2.32.0.min.js" charset="utf-8"></script>
|
<script src="{{ url_for('static', filename='vendor/plotly-2.32.0.min.js') }}" charset="utf-8"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -132,3 +132,47 @@ def test_non_admin_cannot_access(client, mock_admin_api):
|
|||||||
sess["user_id"] = 2
|
sess["user_id"] = 2
|
||||||
resp = client.get("/admin/stations", follow_redirects=False)
|
resp = client.get("/admin/stations", follow_redirects=False)
|
||||||
assert resp.status_code in (301, 302)
|
assert resp.status_code in (301, 302)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-row station reset (point 7)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_station_proxy(logged_in_client, mock_admin_api):
|
||||||
|
mock_admin_api.delete.return_value = {"station_id": 3, "removed": 2}
|
||||||
|
resp = logged_in_client.delete("/admin/api/stations/3/recipes")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == {"station_id": 3, "removed": 2}
|
||||||
|
mock_admin_api.delete.assert_called_once_with("/api/stations/3/recipes")
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_station_propagates_error(logged_in_client, mock_admin_api):
|
||||||
|
mock_admin_api.delete.return_value = {
|
||||||
|
"error": True, "status_code": 404, "detail": "Station not found",
|
||||||
|
}
|
||||||
|
resp = logged_in_client.delete("/admin/api/stations/999/recipes")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_station_distinct_from_single_unassign(logged_in_client, mock_admin_api):
|
||||||
|
"""The collection route must not swallow the per-recipe delete."""
|
||||||
|
mock_admin_api.delete.return_value = {}
|
||||||
|
logged_in_client.delete("/admin/api/stations/3/recipes/7")
|
||||||
|
mock_admin_api.delete.assert_called_once_with("/api/stations/3/recipes/7")
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_list_page_renders_assigned_recipes(logged_in_client, mock_admin_api):
|
||||||
|
"""Recipes assigned to a station reach the page so the row can show them."""
|
||||||
|
mock_admin_api.get.side_effect = [
|
||||||
|
[{
|
||||||
|
"id": 1, "code": "ST-001", "name": "Linea A", "location": "Reparto A",
|
||||||
|
"notes": None, "active": True, "created_by": 1,
|
||||||
|
"created_at": "2026-04-25T10:00:00",
|
||||||
|
"recipes": [{"id": 9, "code": "REC-ABC", "name": "Perno", "active": True}],
|
||||||
|
}],
|
||||||
|
[{"id": 9, "code": "REC-ABC", "name": "Perno", "active": True}],
|
||||||
|
]
|
||||||
|
resp = logged_in_client.get("/admin/stations")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"REC-ABC" in resp.data
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Tests for the production-run proxy routes.
|
||||||
|
|
||||||
|
These are what let task_execute.html read its state back from the server instead of
|
||||||
|
keeping the timer in an Alpine variable that a change of task destroys.
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _restore_measure_module():
|
||||||
|
"""Reload config/measure after each test so env tweaks don't leak."""
|
||||||
|
yield
|
||||||
|
import config
|
||||||
|
importlib.reload(config)
|
||||||
|
import blueprints.measure
|
||||||
|
importlib.reload(blueprints.measure)
|
||||||
|
|
||||||
|
|
||||||
|
def _with_station(monkeypatch, station_code="ST-PROD"):
|
||||||
|
if station_code is None:
|
||||||
|
monkeypatch.delenv("STATION_CODE", raising=False)
|
||||||
|
else:
|
||||||
|
monkeypatch.setenv("STATION_CODE", station_code)
|
||||||
|
monkeypatch.setenv("STATION_SWITCH_ENABLED", "0")
|
||||||
|
import config
|
||||||
|
importlib.reload(config)
|
||||||
|
import blueprints.measure
|
||||||
|
importlib.reload(blueprints.measure)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
return measure_mod
|
||||||
|
|
||||||
|
|
||||||
|
RUN = {
|
||||||
|
"id": 7,
|
||||||
|
"station_id": 1,
|
||||||
|
"recipe_id": 3,
|
||||||
|
"version_id": 5,
|
||||||
|
"operator_id": 2,
|
||||||
|
"lot_number": "LOT-1",
|
||||||
|
"serial_number": None,
|
||||||
|
"status": "running",
|
||||||
|
"measurement_interval_minutes": 30,
|
||||||
|
"next_measurement_at": "2026-07-28T17:00:00",
|
||||||
|
"cycle_count": 2,
|
||||||
|
"started_at": "2026-07-28T16:00:00",
|
||||||
|
"paused_at": None,
|
||||||
|
"closed_at": None,
|
||||||
|
"closed_by": None,
|
||||||
|
"seconds_to_next_measurement": 1500,
|
||||||
|
"overdue": False,
|
||||||
|
"server_time": "2026-07-28T16:35:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_production_passes_the_station(logged_in_client, monkeypatch):
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get.return_value = RUN
|
||||||
|
resp = logged_in_client.get("/measure/api/production/current")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json()["id"] == 7
|
||||||
|
mock_api.get.assert_called_once_with(
|
||||||
|
"/api/production-runs/current", params={"station_code": "ST-PROD"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_production_returns_null_when_idle(logged_in_client, monkeypatch):
|
||||||
|
"""No production open is a normal answer, not an error."""
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get.return_value = None
|
||||||
|
resp = logged_in_client.get("/measure/api/production/current")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_production_without_station_is_503(logged_in_client, monkeypatch):
|
||||||
|
_with_station(monkeypatch, station_code=None)
|
||||||
|
resp = logged_in_client.get("/measure/api/production/current")
|
||||||
|
assert resp.status_code == 503
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_production_sends_station_and_recipe(logged_in_client, monkeypatch):
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.post.return_value = RUN
|
||||||
|
resp = logged_in_client.post(
|
||||||
|
"/measure/api/production/start",
|
||||||
|
json={"recipe_id": 3, "version_id": 5, "lot_number": "LOT-1"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
endpoint, kwargs = mock_api.post.call_args
|
||||||
|
assert endpoint[0] == "/api/production-runs"
|
||||||
|
sent = kwargs["data"]
|
||||||
|
assert sent["station_code"] == "ST-PROD"
|
||||||
|
assert sent["recipe_id"] == 3
|
||||||
|
assert sent["version_id"] == 5
|
||||||
|
assert sent["lot_number"] == "LOT-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_production_falls_back_to_session_traceability(logged_in_client, monkeypatch):
|
||||||
|
"""Lot and serial already captured in the session must reach the run."""
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with logged_in_client.session_transaction() as sess:
|
||||||
|
sess["lot_number"] = "LOT-SESSION"
|
||||||
|
sess["serial_number"] = "SN-SESSION"
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.post.return_value = RUN
|
||||||
|
logged_in_client.post("/measure/api/production/start", json={"recipe_id": 3})
|
||||||
|
sent = mock_api.post.call_args[1]["data"]
|
||||||
|
assert sent["lot_number"] == "LOT-SESSION"
|
||||||
|
assert sent["serial_number"] == "SN-SESSION"
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_production_propagates_conflict(logged_in_client, monkeypatch):
|
||||||
|
"""A station already running a production must not silently open a second."""
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.post.return_value = {
|
||||||
|
"error": True, "status_code": 409,
|
||||||
|
"detail": "Station 'ST-PROD' already has an open production run",
|
||||||
|
}
|
||||||
|
resp = logged_in_client.post(
|
||||||
|
"/measure/api/production/start", json={"recipe_id": 3},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_complete_cycle_hits_the_run(logged_in_client, monkeypatch):
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.post.return_value = {**RUN, "cycle_count": 3}
|
||||||
|
resp = logged_in_client.post("/measure/api/production/7/cycle", json={})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json()["cycle_count"] == 3
|
||||||
|
endpoint, kwargs = mock_api.post.call_args
|
||||||
|
assert endpoint[0] == "/api/production-runs/7/cycle"
|
||||||
|
|
||||||
|
|
||||||
|
def test_complete_cycle_propagates_paused_conflict(logged_in_client, monkeypatch):
|
||||||
|
measure_mod = _with_station(monkeypatch)
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.post.return_value = {
|
||||||
|
"error": True, "status_code": 409,
|
||||||
|
"detail": "Production run is paused: resume it before measuring",
|
||||||
|
}
|
||||||
|
resp = logged_in_client.post("/measure/api/production/7/cycle", json={})
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_production_routes_require_login(client, monkeypatch):
|
||||||
|
_with_station(monkeypatch)
|
||||||
|
for method, url in (
|
||||||
|
("get", "/measure/api/production/current"),
|
||||||
|
("post", "/measure/api/production/start"),
|
||||||
|
("post", "/measure/api/production/7/cycle"),
|
||||||
|
):
|
||||||
|
resp = getattr(client, method)(url)
|
||||||
|
assert resp.status_code in (302, 401), f"{method} {url} -> {resp.status_code}"
|
||||||
@@ -2,13 +2,32 @@
|
|||||||
import importlib
|
import importlib
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
def _reload_measure(monkeypatch, station_code=None):
|
|
||||||
"""Reload config and measure module under the given STATION_CODE env."""
|
@pytest.fixture(autouse=True)
|
||||||
|
def _restore_measure_module():
|
||||||
|
"""Reload config/measure after each test so env tweaks don't leak.
|
||||||
|
|
||||||
|
Reloading runs the module body in its existing namespace, so the view
|
||||||
|
functions already registered on the app see the refreshed globals. Without
|
||||||
|
this teardown a station code set here would stay visible to later tests.
|
||||||
|
Autouse fixtures tear down after monkeypatch, so the env is pristine by then.
|
||||||
|
"""
|
||||||
|
yield
|
||||||
|
import config
|
||||||
|
importlib.reload(config)
|
||||||
|
import blueprints.measure
|
||||||
|
importlib.reload(blueprints.measure)
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_measure(monkeypatch, station_code=None, switch_enabled=False):
|
||||||
|
"""Reload config and measure module under the given station env."""
|
||||||
if station_code is None:
|
if station_code is None:
|
||||||
monkeypatch.delenv("STATION_CODE", raising=False)
|
monkeypatch.delenv("STATION_CODE", raising=False)
|
||||||
else:
|
else:
|
||||||
monkeypatch.setenv("STATION_CODE", station_code)
|
monkeypatch.setenv("STATION_CODE", station_code)
|
||||||
|
monkeypatch.setenv("STATION_SWITCH_ENABLED", "1" if switch_enabled else "0")
|
||||||
import config
|
import config
|
||||||
importlib.reload(config)
|
importlib.reload(config)
|
||||||
import blueprints.measure
|
import blueprints.measure
|
||||||
@@ -35,3 +54,100 @@ def test_select_recipe_without_station_code_shows_error(logged_in_client, monkey
|
|||||||
assert resp.status_code == 503
|
assert resp.status_code == 503
|
||||||
body = resp.data.lower()
|
body = resp.data.lower()
|
||||||
assert b"station_code" in body or b"stazione" in body
|
assert b"station_code" in body or b"stazione" in body
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Switching the current station from the URL (point 7)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _station_calls(mock_api):
|
||||||
|
"""Station codes passed to get_station_recipes, in order."""
|
||||||
|
return [c.args[0] for c in mock_api.get_station_recipes.call_args_list]
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_ignored_when_disabled(logged_in_client, monkeypatch):
|
||||||
|
"""Off by default: on the shop floor the station comes from the install."""
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=False)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
resp = logged_in_client.get("/measure/select?station=ST-OTHER")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# The requested station is never even probed.
|
||||||
|
assert _station_calls(mock_api) == ["ST-CONF"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_applies_when_enabled(logged_in_client, monkeypatch):
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
resp = logged_in_client.get("/measure/select?station=ST-OTHER")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# Probed first to validate, then used to load the recipes.
|
||||||
|
assert _station_calls(mock_api) == ["ST-OTHER", "ST-OTHER"]
|
||||||
|
assert b"ST-OTHER" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_survives_next_request(logged_in_client, monkeypatch):
|
||||||
|
"""The override lives in the session, so it holds without repeating the param."""
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
logged_in_client.get("/measure/select?station=ST-OTHER")
|
||||||
|
mock_api.get_station_recipes.reset_mock()
|
||||||
|
resp = logged_in_client.get("/measure/select")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert _station_calls(mock_api) == ["ST-OTHER"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_rejects_unknown_code(logged_in_client, monkeypatch):
|
||||||
|
"""A typo must not strand the operator on a station that does not exist."""
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.side_effect = [
|
||||||
|
{"error": True, "status_code": 404, "detail": "Station not found"},
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
resp = logged_in_client.get("/measure/select?station=ST-NOPE")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# Probed, refused, then fell back to the configured station.
|
||||||
|
assert _station_calls(mock_api) == ["ST-NOPE", "ST-CONF"]
|
||||||
|
assert b"ST-CONF" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_can_be_cleared(logged_in_client, monkeypatch):
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
logged_in_client.get("/measure/select?station=ST-OTHER")
|
||||||
|
mock_api.get_station_recipes.reset_mock()
|
||||||
|
resp = logged_in_client.get("/measure/select?station=")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert _station_calls(mock_api) == ["ST-CONF"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_is_signalled_in_the_page(logged_in_client, monkeypatch):
|
||||||
|
"""An override must never be silent: wrong-station measurements are untraceable."""
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
resp = logged_in_client.get("/measure/select?station=ST-OTHER")
|
||||||
|
body = resp.data.decode()
|
||||||
|
assert "ST-OTHER" in body
|
||||||
|
# The configured station is named so the deviation is obvious.
|
||||||
|
assert "ST-CONF" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_switch_normalises_case(logged_in_client, monkeypatch):
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-CONF", switch_enabled=True)
|
||||||
|
from blueprints import measure as measure_mod
|
||||||
|
with patch.object(measure_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = []
|
||||||
|
logged_in_client.get("/measure/select?station=st-other")
|
||||||
|
assert _station_calls(mock_api) == ["ST-OTHER", "ST-OTHER"]
|
||||||
|
|||||||
@@ -79,10 +79,8 @@ def _alpine_attribute_expressions(html: str):
|
|||||||
yield name, value
|
yield name, value
|
||||||
|
|
||||||
|
|
||||||
def _node_check(source: str, label: str) -> None:
|
def _node_syntax_error(source: str) -> str | None:
|
||||||
"""Fail the test if `node --check` rejects `source`."""
|
"""Return node's complaint about `source`, or None if it parses."""
|
||||||
if not source.strip():
|
|
||||||
return
|
|
||||||
node = shutil.which("node")
|
node = shutil.which("node")
|
||||||
if node is None:
|
if node is None:
|
||||||
pytest.skip("node binary not found on PATH; cannot validate JS syntax")
|
pytest.skip("node binary not found on PATH; cannot validate JS syntax")
|
||||||
@@ -100,31 +98,56 @@ def _node_check(source: str, label: str) -> None:
|
|||||||
finally:
|
finally:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
if result.returncode != 0:
|
return None if result.returncode == 0 else result.stderr.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _fail(label: str, source: str, stderr: str) -> None:
|
||||||
snippet = source.strip()
|
snippet = source.strip()
|
||||||
if len(snippet) > 600:
|
if len(snippet) > 600:
|
||||||
snippet = snippet[:600] + "\n…(truncated)…"
|
snippet = snippet[:600] + "\n…(truncated)…"
|
||||||
pytest.fail(
|
pytest.fail(
|
||||||
f"{label}: node rejected this code as invalid JS.\n"
|
f"{label}: node rejected this code as invalid JS.\n"
|
||||||
f"--- node stderr ---\n{result.stderr.strip()}\n"
|
f"--- node stderr ---\n{stderr}\n"
|
||||||
f"--- source (first 600 chars) ---\n{snippet}"
|
f"--- source (first 600 chars) ---\n{snippet}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _node_check(source: str, label: str) -> None:
|
||||||
|
"""Fail the test if `node --check` rejects `source`."""
|
||||||
|
if not source.strip():
|
||||||
|
return
|
||||||
|
stderr = _node_syntax_error(source)
|
||||||
|
if stderr is not None:
|
||||||
|
_fail(label, source, stderr)
|
||||||
|
|
||||||
|
|
||||||
def _check_alpine_attributes(html: str, page_label: str) -> None:
|
def _check_alpine_attributes(html: str, page_label: str) -> None:
|
||||||
"""Validate every Alpine expression attribute on the page.
|
"""Validate every Alpine expression attribute on the page.
|
||||||
|
|
||||||
Wraps each value in `void (…)` so node parses it as an expression rather
|
Alpine accepts an attribute either as a single expression or as a sequence of
|
||||||
than a statement. Function-body forms like `async () => { … }` parse fine
|
statements, so the check accepts a value that parses as either and only fails
|
||||||
inside that wrapper too.
|
when both readings are rejected. Neither form alone is enough:
|
||||||
|
|
||||||
|
:class="{ 'dark': $store.theme.dark }" only as an expression
|
||||||
|
@click="showOverlay = false; confirmed = true" only as statements
|
||||||
|
|
||||||
|
An unterminated string literal - the bug this file exists for - is a syntax
|
||||||
|
error under both, so the guard is unchanged.
|
||||||
"""
|
"""
|
||||||
for name, value in _alpine_attribute_expressions(html):
|
for name, value in _alpine_attribute_expressions(html):
|
||||||
# Some Alpine attrs accept a function call shorthand (e.g.
|
as_expression = f"void (\n{value}\n);\n"
|
||||||
# x-data="myComponent(window.__x)"); those parse fine as expressions.
|
as_statements = f"(() => {{\n{value}\n}});\n"
|
||||||
wrapper = f"void ({value});\n"
|
|
||||||
_node_check(
|
expression_error = _node_syntax_error(as_expression)
|
||||||
wrapper,
|
if expression_error is None:
|
||||||
f"{page_label} attribute {name}=\"…\" did not parse as JS",
|
continue
|
||||||
|
if _node_syntax_error(as_statements) is None:
|
||||||
|
continue
|
||||||
|
_fail(
|
||||||
|
f"{page_label} attribute {name}=\"…\" parsed neither as an expression "
|
||||||
|
"nor as statements",
|
||||||
|
as_expression,
|
||||||
|
expression_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,3 +215,52 @@ def test_admin_users_inline_js_is_valid(logged_in_client, mock_admin_api):
|
|||||||
_node_check(body, f"/admin/users script[{i}]")
|
_node_check(body, f"/admin/users script[{i}]")
|
||||||
|
|
||||||
_check_alpine_attributes(html, "/admin/users")
|
_check_alpine_attributes(html, "/admin/users")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_measure_api():
|
||||||
|
"""Patch api_client used inside the measure blueprint."""
|
||||||
|
mock = MagicMock()
|
||||||
|
with patch("blueprints.measure.api_client", mock):
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
|
||||||
|
"""The measurement screen carries the most inline JS in the app.
|
||||||
|
|
||||||
|
It drives the numpad, the caliper burst detection, the supervisor modal and -
|
||||||
|
since the production run moved server-side - the countdown. A broken literal
|
||||||
|
here kills every binding on the screen the operator actually works in.
|
||||||
|
"""
|
||||||
|
_force_italian(logged_in_client)
|
||||||
|
task = {
|
||||||
|
"id": 11,
|
||||||
|
"recipe_id": 3,
|
||||||
|
"version_id": 5,
|
||||||
|
"title": "Quota d'ingresso",
|
||||||
|
"description": "Misura l'altezza",
|
||||||
|
"file_path": None,
|
||||||
|
"file_type": None,
|
||||||
|
"annotations_json": None,
|
||||||
|
"subtasks": [{
|
||||||
|
"id": 21, "marker_number": 1, "name": "Altezza", "order_index": 0,
|
||||||
|
"nominal": 10.0, "utl": 10.5, "uwl": 10.2, "lwl": 9.8, "ltl": 9.5,
|
||||||
|
"unit": "mm",
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
mock_measure_api.get.side_effect = [
|
||||||
|
task, # /api/tasks/11
|
||||||
|
[{"id": 11, "order_index": 0}], # /api/recipes/3/tasks
|
||||||
|
{"id": 3, "measurement_interval_minutes": 30}, # /api/recipes/3
|
||||||
|
]
|
||||||
|
|
||||||
|
resp = logged_in_client.get("/measure/execute/11")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
html = resp.get_data(as_text=True)
|
||||||
|
|
||||||
|
scripts = _INLINE_SCRIPT_RX.findall(html)
|
||||||
|
assert scripts, "expected at least one inline <script> on /measure/execute"
|
||||||
|
for i, body in enumerate(scripts):
|
||||||
|
_node_check(body, f"/measure/execute script[{i}]")
|
||||||
|
|
||||||
|
_check_alpine_attributes(html, "/measure/execute")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-06-11 15:33+0000\n"
|
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -37,7 +37,7 @@ msgstr "Please log in to continue"
|
|||||||
msgid "Inserisci username e password"
|
msgid "Inserisci username e password"
|
||||||
msgstr "Enter username and password"
|
msgstr "Enter username and password"
|
||||||
|
|
||||||
#: blueprints/auth.py:81 blueprints/measure.py:346
|
#: blueprints/auth.py:81 blueprints/measure.py:407
|
||||||
msgid "Credenziali non valide"
|
msgid "Credenziali non valide"
|
||||||
msgstr "Invalid credentials"
|
msgstr "Invalid credentials"
|
||||||
|
|
||||||
@@ -101,65 +101,93 @@ msgstr "No file uploaded"
|
|||||||
msgid "Nome file vuoto"
|
msgid "Nome file vuoto"
|
||||||
msgstr "Empty file name"
|
msgstr "Empty file name"
|
||||||
|
|
||||||
|
#: blueprints/measure.py:45
|
||||||
|
msgid "Il cambio stazione da URL non è abilitato su questa postazione."
|
||||||
|
msgstr "Switching station from the URL is not enabled on this workstation."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:50
|
||||||
|
msgid "Stazione riportata a quella configurata."
|
||||||
|
msgstr "Station restored to the configured one."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:56
|
||||||
|
msgid "Impossibile contattare il server per cambiare stazione."
|
||||||
|
msgstr "Could not reach the server to switch station."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:61
|
||||||
|
#, python-format
|
||||||
|
msgid "Stazione '%(code)s' inesistente o disattivata: stazione invariata."
|
||||||
|
msgstr "Station '%(code)s' does not exist or is disabled: station unchanged."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:68
|
||||||
|
#, python-format
|
||||||
|
msgid "Stazione corrente: %(code)s"
|
||||||
|
msgstr "Current station: %(code)s"
|
||||||
|
|
||||||
# Measure - API Errors
|
# Measure - API Errors
|
||||||
#: blueprints/measure.py:38
|
#: blueprints/measure.py:96
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
||||||
msgstr "Error loading recipes: %(detail)s"
|
msgstr "Error loading recipes: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:86 blueprints/measure.py:172
|
#: blueprints/measure.py:146 blueprints/measure.py:232
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ricetta non trovata: %(detail)s"
|
msgid "Ricetta non trovata: %(detail)s"
|
||||||
msgstr "Recipe not found: %(detail)s"
|
msgstr "Recipe not found: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:96
|
#: blueprints/measure.py:156
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Errore nel caricamento dei task: %(detail)s"
|
msgid "Errore nel caricamento dei task: %(detail)s"
|
||||||
msgstr "Error loading tasks: %(detail)s"
|
msgstr "Error loading tasks: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:125
|
#: blueprints/measure.py:185
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Task non trovato: %(detail)s"
|
msgid "Task non trovato: %(detail)s"
|
||||||
msgstr "Task not found: %(detail)s"
|
msgstr "Task not found: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:241
|
#: blueprints/measure.py:301
|
||||||
msgid "Codice non fornito"
|
msgid "Codice non fornito"
|
||||||
msgstr "Code not provided"
|
msgstr "Code not provided"
|
||||||
|
|
||||||
#: blueprints/measure.py:247 templates/measure/select_recipe.html:48
|
#: blueprints/measure.py:307 templates/measure/select_recipe.html:48
|
||||||
msgid "Ricetta non trovata"
|
msgid "Ricetta non trovata"
|
||||||
msgstr "Recipe not found"
|
msgstr "Recipe not found"
|
||||||
|
|
||||||
#: blueprints/measure.py:303
|
#: blueprints/measure.py:363
|
||||||
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
||||||
msgstr "Missing data: subtask_id, version_id and value are required"
|
msgstr "Missing data: subtask_id, version_id and value are required"
|
||||||
|
|
||||||
#: blueprints/measure.py:322 templates/admin/stations.html:485
|
#: blueprints/measure.py:383 templates/admin/stations.html:549
|
||||||
#: templates/maker/recipe_editor.html:543
|
#: templates/maker/recipe_editor.html:543
|
||||||
msgid "Errore nel salvataggio"
|
msgid "Errore nel salvataggio"
|
||||||
msgstr "Error saving"
|
msgstr "Error saving"
|
||||||
|
|
||||||
#: blueprints/measure.py:341
|
#: blueprints/measure.py:402
|
||||||
msgid "Username e password richiesti"
|
msgid "Username e password richiesti"
|
||||||
msgstr "Username and password required"
|
msgstr "Username and password required"
|
||||||
|
|
||||||
#: blueprints/measure.py:351
|
#: blueprints/measure.py:412
|
||||||
msgid "Utente non autorizzato (richiesto capoturno)"
|
msgid "Utente non autorizzato (richiesto capoturno)"
|
||||||
msgstr "User not authorized (shift supervisor required)"
|
msgstr "User not authorized (shift supervisor required)"
|
||||||
|
|
||||||
#: templates/base.html:175
|
#: blueprints/measure.py:431 blueprints/measure.py:448
|
||||||
|
#: templates/errors/station_not_configured.html:2
|
||||||
|
#: templates/errors/station_not_configured.html:16
|
||||||
|
msgid "Stazione non configurata"
|
||||||
|
msgstr "Station not configured"
|
||||||
|
|
||||||
|
#: templates/base.html:173
|
||||||
msgid "Sessione in scadenza"
|
msgid "Sessione in scadenza"
|
||||||
msgstr "Session expiring"
|
msgstr "Session expiring"
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "Sarai disconnesso tra"
|
msgid "Sarai disconnesso tra"
|
||||||
msgstr "You will be logged out in"
|
msgstr "You will be logged out in"
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "secondi per inattività."
|
msgid "secondi per inattività."
|
||||||
msgstr "seconds due to inactivity."
|
msgstr "seconds due to inactivity."
|
||||||
|
|
||||||
#: templates/base.html:181
|
#: templates/base.html:179
|
||||||
msgid "Continua a lavorare"
|
msgid "Continua a lavorare"
|
||||||
msgstr "Keep working"
|
msgstr "Keep working"
|
||||||
|
|
||||||
@@ -203,16 +231,16 @@ msgstr "Saved"
|
|||||||
msgid "Salva impostazioni"
|
msgid "Salva impostazioni"
|
||||||
msgstr "Save settings"
|
msgstr "Save settings"
|
||||||
|
|
||||||
#: templates/admin/settings.html:107 templates/admin/stations.html:516
|
#: templates/admin/settings.html:107 templates/admin/stations.html:582
|
||||||
#: templates/admin/stations.html:544 templates/admin/stations.html:565
|
#: templates/admin/stations.html:621 templates/admin/stations.html:644
|
||||||
#: templates/admin/stations.html:596 templates/admin/stations.html:618
|
#: templates/admin/stations.html:665 templates/admin/stations.html:700
|
||||||
#: templates/admin/users.html:497 templates/admin/users.html:533
|
#: templates/admin/stations.html:723 templates/admin/users.html:497
|
||||||
#: templates/maker/recipe_editor.html:571 templates/maker/task_drawing.html:65
|
#: templates/admin/users.html:533 templates/maker/recipe_editor.html:571
|
||||||
#: templates/maker/task_editor.html:1243 templates/maker/task_editor.html:1321
|
#: templates/maker/task_drawing.html:65 templates/maker/task_editor.html:1243
|
||||||
#: templates/maker/task_editor.html:1382 templates/maker/task_editor.html:1415
|
#: templates/maker/task_editor.html:1321 templates/maker/task_editor.html:1382
|
||||||
#: templates/maker/task_editor.html:1510 templates/maker/task_editor.html:1586
|
#: templates/maker/task_editor.html:1415 templates/maker/task_editor.html:1510
|
||||||
#: templates/maker/task_editor.html:1660 templates/maker/task_editor.html:1697
|
#: templates/maker/task_editor.html:1586 templates/maker/task_editor.html:1660
|
||||||
#: templates/maker/task_editor.html:1776
|
#: templates/maker/task_editor.html:1697 templates/maker/task_editor.html:1776
|
||||||
#: templates/statistics/dashboard.html:372
|
#: templates/statistics/dashboard.html:372
|
||||||
#: templates/statistics/dashboard.html:457
|
#: templates/statistics/dashboard.html:457
|
||||||
msgid "Errore di connessione al server"
|
msgid "Errore di connessione al server"
|
||||||
@@ -232,209 +260,245 @@ msgstr "Create, edit and manage measurement stations and their assigned recipes"
|
|||||||
msgid "Cerca stazione..."
|
msgid "Cerca stazione..."
|
||||||
msgstr "Search station..."
|
msgstr "Search station..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:39 templates/admin/stations.html:133
|
#: templates/admin/stations.html:39 templates/admin/stations.html:163
|
||||||
msgid "Nuova Stazione"
|
msgid "Nuova Stazione"
|
||||||
msgstr "New Station"
|
msgstr "New Station"
|
||||||
|
|
||||||
# Task Complete Page
|
# Task Complete Page
|
||||||
#: templates/admin/stations.html:49 templates/admin/stations.html:143
|
#: templates/admin/stations.html:49 templates/admin/stations.html:173
|
||||||
#: templates/maker/recipe_editor.html:195
|
#: templates/maker/recipe_editor.html:195
|
||||||
#: templates/measure/task_complete.html:49
|
#: templates/measure/task_complete.html:49
|
||||||
msgid "Codice"
|
msgid "Codice"
|
||||||
msgstr "Code"
|
msgstr "Code"
|
||||||
|
|
||||||
#: templates/admin/stations.html:50 templates/admin/stations.html:160
|
#: templates/admin/stations.html:50 templates/admin/stations.html:190
|
||||||
#: templates/maker/recipe_editor.html:213
|
#: templates/maker/recipe_editor.html:213
|
||||||
#: templates/measure/task_complete.html:53
|
#: templates/measure/task_complete.html:53
|
||||||
msgid "Nome"
|
msgid "Nome"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
#: templates/admin/stations.html:51 templates/admin/stations.html:169
|
#: templates/admin/stations.html:51 templates/admin/stations.html:199
|
||||||
msgid "Postazione"
|
msgid "Postazione"
|
||||||
msgstr "Location"
|
msgstr "Location"
|
||||||
|
|
||||||
#: templates/admin/stations.html:52 templates/admin/users.html:52
|
#: templates/admin/stations.html:52
|
||||||
|
msgid "Ricette collegate"
|
||||||
|
msgstr "Assigned recipes"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:53 templates/admin/users.html:52
|
||||||
#: templates/measure/task_complete.html:292
|
#: templates/measure/task_complete.html:292
|
||||||
msgid "Stato"
|
msgid "Stato"
|
||||||
msgstr "Status"
|
msgstr "Status"
|
||||||
|
|
||||||
#: templates/admin/stations.html:53 templates/admin/users.html:53
|
#: templates/admin/stations.html:54 templates/admin/users.html:53
|
||||||
#: templates/maker/task_editor.html:551
|
#: templates/maker/task_editor.html:551
|
||||||
msgid "Azioni"
|
msgid "Azioni"
|
||||||
msgstr "Actions"
|
msgstr "Actions"
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/admin/stations.html:189
|
#: templates/admin/stations.html:68
|
||||||
|
msgid "Nessuna ricetta"
|
||||||
|
msgstr "No recipes"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:89 templates/admin/stations.html:219
|
||||||
#: templates/maker/recipe_list.html:186
|
#: templates/maker/recipe_list.html:186
|
||||||
msgid "Attiva"
|
msgid "Attiva"
|
||||||
msgstr "Active"
|
msgstr "Active"
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/maker/recipe_list.html:186
|
#: templates/admin/stations.html:89 templates/maker/recipe_list.html:186
|
||||||
msgid "Disattivata"
|
msgid "Disattivata"
|
||||||
msgstr "Inactive"
|
msgstr "Inactive"
|
||||||
|
|
||||||
# Maker - Recipe List
|
# Maker - Recipe List
|
||||||
#: templates/admin/stations.html:76
|
#: templates/admin/stations.html:95
|
||||||
msgid "Gestisci ricette"
|
msgid "Gestisci ricette"
|
||||||
msgstr "Manage recipes"
|
msgstr "Manage recipes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:83 templates/admin/users.html:100
|
#: templates/admin/stations.html:105
|
||||||
|
msgid "Azzera le ricette della stazione"
|
||||||
|
msgstr "Clear the station's recipes"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:106
|
||||||
|
msgid "Nessuna ricetta da azzerare"
|
||||||
|
msgstr "No recipes to clear"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:113 templates/admin/users.html:100
|
||||||
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
||||||
msgid "Modifica"
|
msgid "Modifica"
|
||||||
msgstr "Edit"
|
msgstr "Edit"
|
||||||
|
|
||||||
#: templates/admin/stations.html:90 templates/admin/stations.html:359
|
#: templates/admin/stations.html:120 templates/admin/stations.html:421
|
||||||
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
||||||
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
||||||
msgid "Elimina"
|
msgid "Elimina"
|
||||||
msgstr "Delete"
|
msgstr "Delete"
|
||||||
|
|
||||||
#: templates/admin/stations.html:108
|
#: templates/admin/stations.html:138
|
||||||
msgid "Nessuna stazione trovata"
|
msgid "Nessuna stazione trovata"
|
||||||
msgstr "No stations found"
|
msgstr "No stations found"
|
||||||
|
|
||||||
#: templates/admin/stations.html:114
|
#: templates/admin/stations.html:144
|
||||||
msgid "stazioni"
|
msgid "stazioni"
|
||||||
msgstr "stations"
|
msgstr "stations"
|
||||||
|
|
||||||
#: templates/admin/stations.html:133
|
#: templates/admin/stations.html:163
|
||||||
msgid "Modifica Stazione"
|
msgid "Modifica Stazione"
|
||||||
msgstr "Edit Station"
|
msgstr "Edit Station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:152
|
#: templates/admin/stations.html:182
|
||||||
msgid "Il codice non può essere modificato"
|
msgid "Il codice non può essere modificato"
|
||||||
msgstr "Code cannot be modified"
|
msgstr "Code cannot be modified"
|
||||||
|
|
||||||
#: templates/admin/stations.html:155
|
#: templates/admin/stations.html:185
|
||||||
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
||||||
msgstr "Unique identifier used by client via STATION_CODE"
|
msgstr "Unique identifier used by client via STATION_CODE"
|
||||||
|
|
||||||
#: templates/admin/stations.html:164
|
#: templates/admin/stations.html:194
|
||||||
msgid "Nome descrittivo della stazione"
|
msgid "Nome descrittivo della stazione"
|
||||||
msgstr "Descriptive name of the station"
|
msgstr "Descriptive name of the station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:173
|
#: templates/admin/stations.html:203
|
||||||
msgid "Es. Reparto A - Linea 2"
|
msgid "Es. Reparto A - Linea 2"
|
||||||
msgstr "E.g. Department A - Line 2"
|
msgstr "E.g. Department A - Line 2"
|
||||||
|
|
||||||
#: templates/admin/stations.html:178
|
#: templates/admin/stations.html:208
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr "Notes"
|
msgstr "Notes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:182
|
#: templates/admin/stations.html:212
|
||||||
msgid "Note opzionali"
|
msgid "Note opzionali"
|
||||||
msgstr "Optional notes"
|
msgstr "Optional notes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:204 templates/admin/stations.html:354
|
#: templates/admin/stations.html:234 templates/admin/stations.html:389
|
||||||
#: templates/admin/users.html:292 templates/admin/users.html:323
|
#: templates/admin/stations.html:416 templates/admin/users.html:292
|
||||||
#: templates/maker/recipe_editor.html:137
|
#: templates/admin/users.html:323 templates/maker/recipe_editor.html:137
|
||||||
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
||||||
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
||||||
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
||||||
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
||||||
#: templates/measure/select_recipe.html:350
|
#: templates/measure/select_recipe.html:367
|
||||||
#: templates/measure/task_execute.html:664
|
#: templates/measure/task_execute.html:684
|
||||||
msgid "Annulla"
|
msgid "Annulla"
|
||||||
msgstr "Cancel"
|
msgstr "Cancel"
|
||||||
|
|
||||||
#: templates/admin/stations.html:210 templates/admin/users.html:298
|
#: templates/admin/stations.html:240 templates/admin/users.html:298
|
||||||
#: templates/auth/profile.html:171
|
#: templates/auth/profile.html:171
|
||||||
msgid "Salva Modifiche"
|
msgid "Salva Modifiche"
|
||||||
msgstr "Save Changes"
|
msgstr "Save Changes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:210
|
#: templates/admin/stations.html:240
|
||||||
msgid "Crea Stazione"
|
msgid "Crea Stazione"
|
||||||
msgstr "Create Station"
|
msgstr "Create Station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:211 templates/admin/users.html:299
|
#: templates/admin/stations.html:241 templates/admin/users.html:299
|
||||||
#: templates/maker/recipe_editor.html:107
|
#: templates/maker/recipe_editor.html:107
|
||||||
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
||||||
#: templates/measure/task_execute.html:380
|
#: templates/measure/task_execute.html:380
|
||||||
msgid "Salvataggio..."
|
msgid "Salvataggio..."
|
||||||
msgstr "Saving..."
|
msgstr "Saving..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:228
|
#: templates/admin/stations.html:258
|
||||||
msgid "Ricette Assegnate"
|
msgid "Ricette Assegnate"
|
||||||
msgstr "Assigned Recipes"
|
msgstr "Assigned Recipes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:246
|
#: templates/admin/stations.html:276
|
||||||
msgid "Filtra per codice o nome ricetta..."
|
msgid "Filtra per codice o nome ricetta..."
|
||||||
msgstr "Filter by recipe code or name..."
|
msgstr "Filter by recipe code or name..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:259
|
#: templates/admin/stations.html:289
|
||||||
msgid "Ricette disponibili"
|
msgid "Ricette disponibili"
|
||||||
msgstr "Available recipes"
|
msgstr "Available recipes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:272
|
#: templates/admin/stations.html:302
|
||||||
msgid "Assegna a questa stazione"
|
msgid "Assegna a questa stazione"
|
||||||
msgstr "Assign to this station"
|
msgstr "Assign to this station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:276
|
#: templates/admin/stations.html:306
|
||||||
msgid "Assegna"
|
msgid "Assegna"
|
||||||
msgstr "Assign"
|
msgstr "Assign"
|
||||||
|
|
||||||
#: templates/admin/stations.html:293
|
#: templates/admin/stations.html:323
|
||||||
msgid "Assegnate alla stazione"
|
msgid "Assegnate alla stazione"
|
||||||
msgstr "Assigned to the station"
|
msgstr "Assigned to the station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:305
|
#: templates/admin/stations.html:335
|
||||||
msgid "Rimuovi assegnazione"
|
msgid "Rimuovi assegnazione"
|
||||||
msgstr "Remove assignment"
|
msgstr "Remove assignment"
|
||||||
|
|
||||||
#: templates/admin/stations.html:332
|
#: templates/admin/stations.html:362
|
||||||
#: templates/components/barcode_scanner.html:145
|
#: templates/components/barcode_scanner.html:145
|
||||||
msgid "Chiudi"
|
msgid "Chiudi"
|
||||||
msgstr "Close"
|
msgstr "Close"
|
||||||
|
|
||||||
#: templates/admin/stations.html:345 templates/maker/recipe_list.html:363
|
#: templates/admin/stations.html:376
|
||||||
|
msgid "Azzera Stazione"
|
||||||
|
msgstr "Reset Station"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:378
|
||||||
|
msgid "Rimuovere tutte le ricette assegnate a"
|
||||||
|
msgstr "Remove every recipe assigned to"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:383
|
||||||
|
msgid "assegnazioni verranno rimosse. La stazione resta, e si potrà riassegnare."
|
||||||
|
msgstr "assignments will be removed. The station stays and can be reassigned."
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:394
|
||||||
|
msgid "Azzera"
|
||||||
|
msgstr "Reset"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:407 templates/maker/recipe_list.html:363
|
||||||
msgid "Conferma Eliminazione"
|
msgid "Conferma Eliminazione"
|
||||||
msgstr "Confirm Deletion"
|
msgstr "Confirm Deletion"
|
||||||
|
|
||||||
#: templates/admin/stations.html:347
|
#: templates/admin/stations.html:409
|
||||||
msgid "Sei sicuro di voler eliminare la stazione"
|
msgid "Sei sicuro di voler eliminare la stazione"
|
||||||
msgstr "Are you sure you want to delete the station"
|
msgstr "Are you sure you want to delete the station"
|
||||||
|
|
||||||
#: templates/admin/stations.html:349
|
#: templates/admin/stations.html:411
|
||||||
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
||||||
msgstr "All recipe assignments will also be removed."
|
msgstr "All recipe assignments will also be removed."
|
||||||
|
|
||||||
#: templates/admin/stations.html:430 templates/admin/stations.html:436
|
#: templates/admin/stations.html:494 templates/admin/stations.html:500
|
||||||
msgid "Nessun risultato per il filtro"
|
msgid "Nessun risultato per il filtro"
|
||||||
msgstr "No results for filter"
|
msgstr "No results for filter"
|
||||||
|
|
||||||
#: templates/admin/stations.html:431
|
#: templates/admin/stations.html:495
|
||||||
msgid "Nessuna ricetta nel sistema"
|
msgid "Nessuna ricetta nel sistema"
|
||||||
msgstr "No recipes in system"
|
msgstr "No recipes in system"
|
||||||
|
|
||||||
#: templates/admin/stations.html:432
|
#: templates/admin/stations.html:496
|
||||||
msgid "Tutte le ricette sono già assegnate"
|
msgid "Tutte le ricette sono già assegnate"
|
||||||
msgstr "All recipes are already assigned"
|
msgstr "All recipes are already assigned"
|
||||||
|
|
||||||
#: templates/admin/stations.html:437
|
#: templates/admin/stations.html:501
|
||||||
msgid "Nessuna ricetta assegnata"
|
msgid "Nessuna ricetta assegnata"
|
||||||
msgstr "No assigned recipes"
|
msgstr "No assigned recipes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:492
|
#: templates/admin/stations.html:558
|
||||||
msgid "Codice e nome sono obbligatori"
|
msgid "Codice e nome sono obbligatori"
|
||||||
msgstr "Code and name are required"
|
msgstr "Code and name are required"
|
||||||
|
|
||||||
#: templates/admin/stations.html:509
|
#: templates/admin/stations.html:575
|
||||||
msgid "Errore nella creazione"
|
msgid "Errore nella creazione"
|
||||||
msgstr "Error during creation"
|
msgstr "Error during creation"
|
||||||
|
|
||||||
#: templates/admin/stations.html:537
|
#: templates/admin/stations.html:609
|
||||||
|
msgid "Errore nell azzeramento"
|
||||||
|
msgstr "Error clearing assignments"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:637
|
||||||
msgid "Errore nella eliminazione"
|
msgid "Errore nella eliminazione"
|
||||||
msgstr "Error during deletion"
|
msgstr "Error during deletion"
|
||||||
|
|
||||||
# Maker - API Errors
|
# Maker - API Errors
|
||||||
#: templates/admin/stations.html:562
|
#: templates/admin/stations.html:662
|
||||||
msgid "Errore nel caricamento delle ricette"
|
msgid "Errore nel caricamento delle ricette"
|
||||||
msgstr "Error loading recipes"
|
msgstr "Error loading recipes"
|
||||||
|
|
||||||
#: templates/admin/stations.html:590
|
#: templates/admin/stations.html:690
|
||||||
msgid "Errore nella assegnazione"
|
msgid "Errore nella assegnazione"
|
||||||
msgstr "Error during assignment"
|
msgstr "Error during assignment"
|
||||||
|
|
||||||
#: templates/admin/stations.html:613
|
#: templates/admin/stations.html:717
|
||||||
msgid "Errore nella rimozione"
|
msgid "Errore nella rimozione"
|
||||||
msgstr "Error during removal"
|
msgstr "Error during removal"
|
||||||
|
|
||||||
@@ -455,9 +519,9 @@ msgid "Nuovo Utente"
|
|||||||
msgstr "New User"
|
msgstr "New User"
|
||||||
|
|
||||||
#: templates/admin/users.html:48 templates/admin/users.html:173
|
#: templates/admin/users.html:48 templates/admin/users.html:173
|
||||||
#: templates/admin/users.html:179 templates/auth/login.html:33
|
#: templates/admin/users.html:179 templates/auth/login.html:35
|
||||||
#: templates/auth/login.html:47 templates/auth/profile.html:36
|
#: templates/auth/login.html:49 templates/auth/profile.html:36
|
||||||
#: templates/measure/task_execute.html:649
|
#: templates/measure/task_execute.html:669
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr "Username"
|
msgstr "Username"
|
||||||
|
|
||||||
@@ -508,8 +572,8 @@ msgid "Il nome utente non può essere modificato"
|
|||||||
msgstr "Username cannot be changed"
|
msgstr "Username cannot be changed"
|
||||||
|
|
||||||
#: templates/admin/users.html:206 templates/admin/users.html:214
|
#: templates/admin/users.html:206 templates/admin/users.html:214
|
||||||
#: templates/auth/login.html:55 templates/auth/login.html:69
|
#: templates/auth/login.html:57 templates/auth/login.html:71
|
||||||
#: templates/measure/task_execute.html:654
|
#: templates/measure/task_execute.html:674
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Password"
|
msgstr "Password"
|
||||||
|
|
||||||
@@ -578,36 +642,36 @@ msgid "Username, nome visualizzato e password sono obbligatori"
|
|||||||
msgstr "Username, display name and password are required"
|
msgstr "Username, display name and password are required"
|
||||||
|
|
||||||
# Login Page
|
# Login Page
|
||||||
#: templates/auth/login.html:23
|
#: templates/auth/login.html:25
|
||||||
msgid "Accedi al sistema"
|
msgid "Accedi al sistema"
|
||||||
msgstr "Sign in to your account"
|
msgstr "Sign in to your account"
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Nascondi password"
|
msgid "Nascondi password"
|
||||||
msgstr "Hide password"
|
msgstr "Hide password"
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Mostra password"
|
msgid "Mostra password"
|
||||||
msgstr "Show password"
|
msgstr "Show password"
|
||||||
|
|
||||||
#: templates/auth/login.html:94
|
#: templates/auth/login.html:96
|
||||||
msgid "Accedi"
|
msgid "Accedi"
|
||||||
msgstr "Sign In"
|
msgstr "Sign In"
|
||||||
|
|
||||||
#: templates/auth/login.html:102
|
#: templates/auth/login.html:104
|
||||||
msgid "Hai dimenticato la password?"
|
msgid "Hai dimenticato la password?"
|
||||||
msgstr "Forgot password?"
|
msgstr "Forgot password?"
|
||||||
|
|
||||||
#: templates/auth/login.html:104
|
#: templates/auth/login.html:106
|
||||||
msgid "Contatta l'amministratore"
|
msgid "Contatta l'amministratore"
|
||||||
msgstr "Contact administrator"
|
msgstr "Contact administrator"
|
||||||
|
|
||||||
# Additional Login Page
|
# Additional Login Page
|
||||||
#: templates/auth/login.html:111
|
#: templates/auth/login.html:113
|
||||||
msgid "Sistema di misurazione industriale"
|
msgid "Sistema di misurazione industriale"
|
||||||
msgstr "Industrial measurement system"
|
msgstr "Industrial measurement system"
|
||||||
|
|
||||||
#: templates/auth/profile.html:2 templates/components/navbar.html:147
|
#: templates/auth/profile.html:2 templates/components/navbar.html:149
|
||||||
msgid "Profilo"
|
msgid "Profilo"
|
||||||
msgstr "Profile"
|
msgstr "Profile"
|
||||||
|
|
||||||
@@ -657,15 +721,15 @@ msgstr "Logout from system"
|
|||||||
msgid "Termina la sessione corrente e torna alla schermata di login"
|
msgid "Termina la sessione corrente e torna alla schermata di login"
|
||||||
msgstr "End current session and return to login screen"
|
msgstr "End current session and return to login screen"
|
||||||
|
|
||||||
#: templates/auth/profile.html:197 templates/components/navbar.html:158
|
#: templates/auth/profile.html:197 templates/components/navbar.html:160
|
||||||
#: templates/components/navbar.html:167
|
#: templates/components/navbar.html:169
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Logout"
|
msgstr "Logout"
|
||||||
|
|
||||||
#: templates/components/barcode_scanner.html:21
|
#: templates/components/barcode_scanner.html:21
|
||||||
#: templates/components/barcode_scanner.html:54
|
#: templates/components/barcode_scanner.html:54
|
||||||
#: templates/measure/select_recipe.html:90
|
#: templates/measure/select_recipe.html:107
|
||||||
#: templates/measure/select_recipe.html:302
|
#: templates/measure/select_recipe.html:319
|
||||||
msgid "Scansiona Barcode"
|
msgid "Scansiona Barcode"
|
||||||
msgstr "Scan Barcode"
|
msgstr "Scan Barcode"
|
||||||
|
|
||||||
@@ -751,7 +815,7 @@ msgstr "Users"
|
|||||||
msgid "Stazioni"
|
msgid "Stazioni"
|
||||||
msgstr "Stations"
|
msgstr "Stations"
|
||||||
|
|
||||||
#: templates/components/navbar.html:154
|
#: templates/components/navbar.html:156
|
||||||
msgid "Logout bloccato durante le misurazioni"
|
msgid "Logout bloccato durante le misurazioni"
|
||||||
msgstr "Logout blocked during measurements"
|
msgstr "Logout blocked during measurements"
|
||||||
|
|
||||||
@@ -759,11 +823,6 @@ msgstr "Logout blocked during measurements"
|
|||||||
msgid "Prossima misura"
|
msgid "Prossima misura"
|
||||||
msgstr "Next measurement"
|
msgstr "Next measurement"
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:2
|
|
||||||
#: templates/errors/station_not_configured.html:16
|
|
||||||
msgid "Stazione non configurata"
|
|
||||||
msgstr "Station not configured"
|
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:20
|
#: templates/errors/station_not_configured.html:20
|
||||||
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
||||||
msgstr "This client has not set the STATION_CODE environment variable."
|
msgstr "This client has not set the STATION_CODE environment variable."
|
||||||
@@ -805,7 +864,7 @@ msgstr "Preview"
|
|||||||
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
||||||
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
||||||
#: templates/measure/task_complete.html:168
|
#: templates/measure/task_complete.html:168
|
||||||
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
|
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
|
||||||
#: templates/measure/task_list.html:156
|
#: templates/measure/task_list.html:156
|
||||||
msgid "Task"
|
msgid "Task"
|
||||||
msgstr "Task"
|
msgstr "Task"
|
||||||
@@ -954,7 +1013,8 @@ msgstr "Error during deletion"
|
|||||||
|
|
||||||
# Recipe Selection Additional
|
# Recipe Selection Additional
|
||||||
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
||||||
#: templates/measure/task_execute.html:1111
|
#: templates/measure/task_execute.html:1057
|
||||||
|
#: templates/measure/task_execute.html:1241
|
||||||
msgid "Errore di connessione"
|
msgid "Errore di connessione"
|
||||||
msgstr "Connection Error"
|
msgstr "Connection Error"
|
||||||
|
|
||||||
@@ -987,7 +1047,7 @@ msgid "ricetta trovata"
|
|||||||
msgstr "recipe found"
|
msgstr "recipe found"
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:144
|
#: templates/maker/recipe_list.html:144
|
||||||
#: templates/measure/select_recipe.html:158
|
#: templates/measure/select_recipe.html:175
|
||||||
msgid "ricette trovate"
|
msgid "ricette trovate"
|
||||||
msgstr "recipes found"
|
msgstr "recipes found"
|
||||||
|
|
||||||
@@ -1005,7 +1065,7 @@ msgid "Versioni"
|
|||||||
msgstr "Versions"
|
msgstr "Versions"
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:297
|
#: templates/maker/recipe_list.html:297
|
||||||
#: templates/measure/select_recipe.html:256
|
#: templates/measure/select_recipe.html:273
|
||||||
msgid "Nessuna ricetta trovata"
|
msgid "Nessuna ricetta trovata"
|
||||||
msgstr "No recipe found"
|
msgstr "No recipe found"
|
||||||
|
|
||||||
@@ -1149,7 +1209,7 @@ msgid "Torna ai Task"
|
|||||||
msgstr "Back to Tasks"
|
msgstr "Back to Tasks"
|
||||||
|
|
||||||
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
||||||
#: templates/measure/select_recipe.html:236
|
#: templates/measure/select_recipe.html:253
|
||||||
msgid "Seleziona"
|
msgid "Seleziona"
|
||||||
msgstr "Select"
|
msgstr "Select"
|
||||||
|
|
||||||
@@ -1552,7 +1612,7 @@ msgid "Misurazione aggiunta"
|
|||||||
msgstr "Measurement added"
|
msgstr "Measurement added"
|
||||||
|
|
||||||
#: templates/maker/task_editor.html:1645
|
#: templates/maker/task_editor.html:1645
|
||||||
#: templates/measure/task_execute.html:862
|
#: templates/measure/task_execute.html:931
|
||||||
msgid "Errore nel salvataggio della misurazione"
|
msgid "Errore nel salvataggio della misurazione"
|
||||||
msgstr "Error saving measurement"
|
msgstr "Error saving measurement"
|
||||||
|
|
||||||
@@ -1627,84 +1687,96 @@ msgstr "Choose the measurement recipe to execute"
|
|||||||
msgid "Stazione"
|
msgid "Stazione"
|
||||||
msgstr "Station"
|
msgstr "Station"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:102
|
#: templates/measure/select_recipe.html:89
|
||||||
|
msgid "Stazione forzata per collaudo"
|
||||||
|
msgstr "Station overridden for commissioning"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:89
|
||||||
|
msgid "configurata"
|
||||||
|
msgstr "configured"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:94
|
||||||
|
msgid "Ripristina"
|
||||||
|
msgstr "Restore"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:119
|
||||||
msgid "Cerca ricetta"
|
msgid "Cerca ricetta"
|
||||||
msgstr "Search recipe"
|
msgstr "Search recipe"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:106
|
#: templates/measure/select_recipe.html:123
|
||||||
msgid "Nome, codice o descrizione..."
|
msgid "Nome, codice o descrizione..."
|
||||||
msgstr "Name, code or description..."
|
msgstr "Name, code or description..."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:119
|
#: templates/measure/select_recipe.html:136
|
||||||
msgid "Tracciabilità"
|
msgid "Tracciabilità"
|
||||||
msgstr "Traceability"
|
msgstr "Traceability"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:120
|
#: templates/measure/select_recipe.html:137
|
||||||
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
||||||
msgstr "Data of the part to measure — fill in before selecting the recipe"
|
msgstr "Data of the part to measure — fill in before selecting the recipe"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:130
|
#: templates/measure/select_recipe.html:147
|
||||||
#: templates/measure/task_complete.html:281
|
#: templates/measure/task_complete.html:281
|
||||||
msgid "Numero Lotto"
|
msgid "Numero Lotto"
|
||||||
msgstr "Lot Number"
|
msgstr "Lot Number"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:134
|
#: templates/measure/select_recipe.html:151
|
||||||
msgid "Es. LOT-2026-001 (opzionale)"
|
msgid "Es. LOT-2026-001 (opzionale)"
|
||||||
msgstr "E.g. LOT-2026-001 (optional)"
|
msgstr "E.g. LOT-2026-001 (optional)"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:144
|
#: templates/measure/select_recipe.html:161
|
||||||
#: templates/measure/task_complete.html:282
|
#: templates/measure/task_complete.html:282
|
||||||
msgid "Numero Seriale"
|
msgid "Numero Seriale"
|
||||||
msgstr "Serial Number"
|
msgstr "Serial Number"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:148
|
#: templates/measure/select_recipe.html:165
|
||||||
msgid "Es. SN-000123 (opzionale)"
|
msgid "Es. SN-000123 (opzionale)"
|
||||||
msgstr "E.g. SN-000123 (optional)"
|
msgstr "E.g. SN-000123 (optional)"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:201
|
#: templates/measure/select_recipe.html:218
|
||||||
msgid "Nessuna descrizione disponibile"
|
msgid "Nessuna descrizione disponibile"
|
||||||
msgstr "No description available"
|
msgstr "No description available"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:260
|
#: templates/measure/select_recipe.html:277
|
||||||
msgid "Nessun risultato per"
|
msgid "Nessun risultato per"
|
||||||
msgstr "No results for"
|
msgstr "No results for"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:261
|
#: templates/measure/select_recipe.html:278
|
||||||
msgid "Prova con un termine diverso."
|
msgid "Prova con un termine diverso."
|
||||||
msgstr "Try a different search term."
|
msgstr "Try a different search term."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:264
|
#: templates/measure/select_recipe.html:281
|
||||||
msgid "Non ci sono ricette disponibili al momento."
|
msgid "Non ci sono ricette disponibili al momento."
|
||||||
msgstr "No recipes available at the moment."
|
msgstr "No recipes available at the moment."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:317
|
#: templates/measure/select_recipe.html:334
|
||||||
msgid ""
|
msgid ""
|
||||||
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
||||||
"automaticamente."
|
"automaticamente."
|
||||||
msgstr "Enter or scan the recipe code to select it automatically."
|
msgstr "Enter or scan the recipe code to select it automatically."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:322
|
#: templates/measure/select_recipe.html:339
|
||||||
msgid "Codice Ricetta"
|
msgid "Codice Ricetta"
|
||||||
msgstr "Recipe Code"
|
msgstr "Recipe Code"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:328
|
#: templates/measure/select_recipe.html:345
|
||||||
msgid "Es. REC-001"
|
msgid "Es. REC-001"
|
||||||
msgstr "E.g. REC-001"
|
msgstr "E.g. REC-001"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:364
|
#: templates/measure/select_recipe.html:381
|
||||||
msgid "Cerca"
|
msgid "Cerca"
|
||||||
msgstr "Search"
|
msgstr "Search"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:3
|
#: templates/measure/task_complete.html:3
|
||||||
#: templates/measure/task_complete.html:36
|
#: templates/measure/task_complete.html:36
|
||||||
#: templates/measure/task_execute.html:112
|
#: templates/measure/task_execute.html:112
|
||||||
#: templates/measure/task_execute.html:585
|
#: templates/measure/task_execute.html:605
|
||||||
#: templates/statistics/dashboard.html:139
|
#: templates/statistics/dashboard.html:139
|
||||||
msgid "Riepilogo"
|
msgid "Riepilogo"
|
||||||
msgstr "Summary"
|
msgstr "Summary"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:44
|
#: templates/measure/task_complete.html:44
|
||||||
#: templates/measure/task_execute.html:562
|
#: templates/measure/task_execute.html:582
|
||||||
msgid "Misurazioni Complete"
|
msgid "Misurazioni Complete"
|
||||||
msgstr "Measurements Complete"
|
msgstr "Measurements Complete"
|
||||||
|
|
||||||
@@ -1725,12 +1797,12 @@ msgid "Totale"
|
|||||||
msgstr "Total"
|
msgstr "Total"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:103
|
#: templates/measure/task_complete.html:103
|
||||||
#: templates/measure/task_execute.html:570
|
#: templates/measure/task_execute.html:590
|
||||||
msgid "Conformi"
|
msgid "Conformi"
|
||||||
msgstr "Pass"
|
msgstr "Pass"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:120
|
#: templates/measure/task_complete.html:120
|
||||||
#: templates/measure/task_execute.html:574
|
#: templates/measure/task_execute.html:594
|
||||||
msgid "Attenzione"
|
msgid "Attenzione"
|
||||||
msgstr "Warning"
|
msgstr "Warning"
|
||||||
|
|
||||||
@@ -1930,77 +2002,85 @@ msgstr "Next measurement in"
|
|||||||
msgid "Ciclo"
|
msgid "Ciclo"
|
||||||
msgstr "Cycle"
|
msgstr "Cycle"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:446
|
#: templates/measure/task_execute.html:445
|
||||||
#: templates/measure/task_execute.html:612
|
msgid "Produzione non registrata sul server"
|
||||||
|
msgstr "Production not recorded on the server"
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:466
|
||||||
|
#: templates/measure/task_execute.html:632
|
||||||
msgid "Avvio Produzione"
|
msgid "Avvio Produzione"
|
||||||
msgstr "Production Start"
|
msgstr "Production Start"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:450
|
#: templates/measure/task_execute.html:470
|
||||||
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
||||||
msgstr "Send a signal to the ERP system to start the line timer"
|
msgstr "Send a signal to the ERP system to start the line timer"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:461
|
#: templates/measure/task_execute.html:481
|
||||||
msgid "Produzione avviata"
|
msgid "Produzione avviata"
|
||||||
msgstr "Production started"
|
msgstr "Production started"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:506
|
#: templates/measure/task_execute.html:526
|
||||||
msgid "Fine ciclo misura"
|
msgid "Fine ciclo misura"
|
||||||
msgstr "Measurement cycle complete"
|
msgstr "Measurement cycle complete"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:516
|
#: templates/measure/task_execute.html:536
|
||||||
#: templates/measure/task_execute.html:530
|
#: templates/measure/task_execute.html:550
|
||||||
msgid "Completato"
|
msgid "Completato"
|
||||||
msgstr "Completed"
|
msgstr "Completed"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "Tutte le"
|
msgid "Tutte le"
|
||||||
msgstr "All"
|
msgstr "All"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "misurazioni sono state registrate."
|
msgid "misurazioni sono state registrate."
|
||||||
msgstr "measurements have been recorded."
|
msgstr "measurements have been recorded."
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:578
|
#: templates/measure/task_execute.html:598
|
||||||
msgid "Non Conf."
|
msgid "Non Conf."
|
||||||
msgstr "Fail"
|
msgstr "Fail"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:592
|
#: templates/measure/task_execute.html:612
|
||||||
msgid "Conferma ciclo"
|
msgid "Conferma ciclo"
|
||||||
msgstr "Confirm cycle"
|
msgstr "Confirm cycle"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:602
|
#: templates/measure/task_execute.html:622
|
||||||
msgid "Task successivo"
|
msgid "Task successivo"
|
||||||
msgstr "Next task"
|
msgstr "Next task"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:642
|
#: templates/measure/task_execute.html:662
|
||||||
msgid "Autorizzazione capoturno"
|
msgid "Autorizzazione capoturno"
|
||||||
msgstr "Shift supervisor authorization"
|
msgstr "Shift supervisor authorization"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:651
|
#: templates/measure/task_execute.html:671
|
||||||
msgid "Username capoturno"
|
msgid "Username capoturno"
|
||||||
msgstr "Supervisor username"
|
msgstr "Supervisor username"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:673
|
#: templates/measure/task_execute.html:693
|
||||||
msgid "Autorizza"
|
msgid "Autorizza"
|
||||||
msgstr "Authorize"
|
msgstr "Authorize"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:906
|
#: templates/measure/task_execute.html:975
|
||||||
msgid "Errore di rete. Riprovare."
|
msgid "Errore di rete. Riprovare."
|
||||||
msgstr "Network error. Please retry."
|
msgstr "Network error. Please retry."
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1068
|
#: templates/measure/task_execute.html:1052
|
||||||
|
msgid "Errore di comunicazione con il server"
|
||||||
|
msgstr "Error communicating with the server"
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:1198
|
||||||
msgid "Misurazione fuori tolleranza"
|
msgid "Misurazione fuori tolleranza"
|
||||||
msgstr "Measurement out of tolerance"
|
msgstr "Measurement out of tolerance"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1069
|
#: templates/measure/task_execute.html:1199
|
||||||
msgid "Fermo linea richiesto"
|
msgid "Fermo linea richiesto"
|
||||||
msgstr "Line stop requested"
|
msgstr "Line stop requested"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1070
|
#: templates/measure/task_execute.html:1200
|
||||||
msgid "Fine produzione richiesta"
|
msgid "Fine produzione richiesta"
|
||||||
msgstr "End of production requested"
|
msgstr "End of production requested"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1089
|
#: templates/measure/task_execute.html:1219
|
||||||
msgid "Credenziali non valide o utente non autorizzato"
|
msgid "Credenziali non valide o utente non autorizzato"
|
||||||
msgstr "Invalid credentials or unauthorized user"
|
msgstr "Invalid credentials or unauthorized user"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-06-11 15:33+0000\n"
|
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
@@ -37,7 +37,7 @@ msgstr "Effettua il login per continuare"
|
|||||||
msgid "Inserisci username e password"
|
msgid "Inserisci username e password"
|
||||||
msgstr "Inserisci username e password"
|
msgstr "Inserisci username e password"
|
||||||
|
|
||||||
#: blueprints/auth.py:81 blueprints/measure.py:346
|
#: blueprints/auth.py:81 blueprints/measure.py:407
|
||||||
msgid "Credenziali non valide"
|
msgid "Credenziali non valide"
|
||||||
msgstr "Credenziali non valide"
|
msgstr "Credenziali non valide"
|
||||||
|
|
||||||
@@ -101,65 +101,93 @@ msgstr "Nessun file caricato"
|
|||||||
msgid "Nome file vuoto"
|
msgid "Nome file vuoto"
|
||||||
msgstr "Nome file vuoto"
|
msgstr "Nome file vuoto"
|
||||||
|
|
||||||
|
#: blueprints/measure.py:45
|
||||||
|
msgid "Il cambio stazione da URL non è abilitato su questa postazione."
|
||||||
|
msgstr "Il cambio stazione da URL non è abilitato su questa postazione."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:50
|
||||||
|
msgid "Stazione riportata a quella configurata."
|
||||||
|
msgstr "Stazione riportata a quella configurata."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:56
|
||||||
|
msgid "Impossibile contattare il server per cambiare stazione."
|
||||||
|
msgstr "Impossibile contattare il server per cambiare stazione."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:61
|
||||||
|
#, python-format
|
||||||
|
msgid "Stazione '%(code)s' inesistente o disattivata: stazione invariata."
|
||||||
|
msgstr "Stazione '%(code)s' inesistente o disattivata: stazione invariata."
|
||||||
|
|
||||||
|
#: blueprints/measure.py:68
|
||||||
|
#, python-format
|
||||||
|
msgid "Stazione corrente: %(code)s"
|
||||||
|
msgstr "Stazione corrente: %(code)s"
|
||||||
|
|
||||||
# Measure - API Errors
|
# Measure - API Errors
|
||||||
#: blueprints/measure.py:38
|
#: blueprints/measure.py:96
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
||||||
msgstr "Errore nel caricamento delle ricette: %(detail)s"
|
msgstr "Errore nel caricamento delle ricette: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:86 blueprints/measure.py:172
|
#: blueprints/measure.py:146 blueprints/measure.py:232
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ricetta non trovata: %(detail)s"
|
msgid "Ricetta non trovata: %(detail)s"
|
||||||
msgstr "Ricetta non trovata: %(detail)s"
|
msgstr "Ricetta non trovata: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:96
|
#: blueprints/measure.py:156
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Errore nel caricamento dei task: %(detail)s"
|
msgid "Errore nel caricamento dei task: %(detail)s"
|
||||||
msgstr "Errore nel caricamento dei task: %(detail)s"
|
msgstr "Errore nel caricamento dei task: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:125
|
#: blueprints/measure.py:185
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Task non trovato: %(detail)s"
|
msgid "Task non trovato: %(detail)s"
|
||||||
msgstr "Task non trovato: %(detail)s"
|
msgstr "Task non trovato: %(detail)s"
|
||||||
|
|
||||||
#: blueprints/measure.py:241
|
#: blueprints/measure.py:301
|
||||||
msgid "Codice non fornito"
|
msgid "Codice non fornito"
|
||||||
msgstr "Codice non fornito"
|
msgstr "Codice non fornito"
|
||||||
|
|
||||||
#: blueprints/measure.py:247 templates/measure/select_recipe.html:48
|
#: blueprints/measure.py:307 templates/measure/select_recipe.html:48
|
||||||
msgid "Ricetta non trovata"
|
msgid "Ricetta non trovata"
|
||||||
msgstr "Ricetta non trovata"
|
msgstr "Ricetta non trovata"
|
||||||
|
|
||||||
#: blueprints/measure.py:303
|
#: blueprints/measure.py:363
|
||||||
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
||||||
msgstr "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
msgstr "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
||||||
|
|
||||||
#: blueprints/measure.py:322 templates/admin/stations.html:485
|
#: blueprints/measure.py:383 templates/admin/stations.html:549
|
||||||
#: templates/maker/recipe_editor.html:543
|
#: templates/maker/recipe_editor.html:543
|
||||||
msgid "Errore nel salvataggio"
|
msgid "Errore nel salvataggio"
|
||||||
msgstr "Errore nel salvataggio"
|
msgstr "Errore nel salvataggio"
|
||||||
|
|
||||||
#: blueprints/measure.py:341
|
#: blueprints/measure.py:402
|
||||||
msgid "Username e password richiesti"
|
msgid "Username e password richiesti"
|
||||||
msgstr "Username e password richiesti"
|
msgstr "Username e password richiesti"
|
||||||
|
|
||||||
#: blueprints/measure.py:351
|
#: blueprints/measure.py:412
|
||||||
msgid "Utente non autorizzato (richiesto capoturno)"
|
msgid "Utente non autorizzato (richiesto capoturno)"
|
||||||
msgstr "Utente non autorizzato (richiesto capoturno)"
|
msgstr "Utente non autorizzato (richiesto capoturno)"
|
||||||
|
|
||||||
#: templates/base.html:175
|
#: blueprints/measure.py:431 blueprints/measure.py:448
|
||||||
|
#: templates/errors/station_not_configured.html:2
|
||||||
|
#: templates/errors/station_not_configured.html:16
|
||||||
|
msgid "Stazione non configurata"
|
||||||
|
msgstr "Stazione non configurata"
|
||||||
|
|
||||||
|
#: templates/base.html:173
|
||||||
msgid "Sessione in scadenza"
|
msgid "Sessione in scadenza"
|
||||||
msgstr "Sessione in scadenza"
|
msgstr "Sessione in scadenza"
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "Sarai disconnesso tra"
|
msgid "Sarai disconnesso tra"
|
||||||
msgstr "Sarai disconnesso tra"
|
msgstr "Sarai disconnesso tra"
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "secondi per inattività."
|
msgid "secondi per inattività."
|
||||||
msgstr "secondi per inattività."
|
msgstr "secondi per inattività."
|
||||||
|
|
||||||
#: templates/base.html:181
|
#: templates/base.html:179
|
||||||
msgid "Continua a lavorare"
|
msgid "Continua a lavorare"
|
||||||
msgstr "Continua a lavorare"
|
msgstr "Continua a lavorare"
|
||||||
|
|
||||||
@@ -203,16 +231,16 @@ msgstr "Salvato"
|
|||||||
msgid "Salva impostazioni"
|
msgid "Salva impostazioni"
|
||||||
msgstr "Salva impostazioni"
|
msgstr "Salva impostazioni"
|
||||||
|
|
||||||
#: templates/admin/settings.html:107 templates/admin/stations.html:516
|
#: templates/admin/settings.html:107 templates/admin/stations.html:582
|
||||||
#: templates/admin/stations.html:544 templates/admin/stations.html:565
|
#: templates/admin/stations.html:621 templates/admin/stations.html:644
|
||||||
#: templates/admin/stations.html:596 templates/admin/stations.html:618
|
#: templates/admin/stations.html:665 templates/admin/stations.html:700
|
||||||
#: templates/admin/users.html:497 templates/admin/users.html:533
|
#: templates/admin/stations.html:723 templates/admin/users.html:497
|
||||||
#: templates/maker/recipe_editor.html:571 templates/maker/task_drawing.html:65
|
#: templates/admin/users.html:533 templates/maker/recipe_editor.html:571
|
||||||
#: templates/maker/task_editor.html:1243 templates/maker/task_editor.html:1321
|
#: templates/maker/task_drawing.html:65 templates/maker/task_editor.html:1243
|
||||||
#: templates/maker/task_editor.html:1382 templates/maker/task_editor.html:1415
|
#: templates/maker/task_editor.html:1321 templates/maker/task_editor.html:1382
|
||||||
#: templates/maker/task_editor.html:1510 templates/maker/task_editor.html:1586
|
#: templates/maker/task_editor.html:1415 templates/maker/task_editor.html:1510
|
||||||
#: templates/maker/task_editor.html:1660 templates/maker/task_editor.html:1697
|
#: templates/maker/task_editor.html:1586 templates/maker/task_editor.html:1660
|
||||||
#: templates/maker/task_editor.html:1776
|
#: templates/maker/task_editor.html:1697 templates/maker/task_editor.html:1776
|
||||||
#: templates/statistics/dashboard.html:372
|
#: templates/statistics/dashboard.html:372
|
||||||
#: templates/statistics/dashboard.html:457
|
#: templates/statistics/dashboard.html:457
|
||||||
msgid "Errore di connessione al server"
|
msgid "Errore di connessione al server"
|
||||||
@@ -234,209 +262,245 @@ msgstr ""
|
|||||||
msgid "Cerca stazione..."
|
msgid "Cerca stazione..."
|
||||||
msgstr "Cerca stazione..."
|
msgstr "Cerca stazione..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:39 templates/admin/stations.html:133
|
#: templates/admin/stations.html:39 templates/admin/stations.html:163
|
||||||
msgid "Nuova Stazione"
|
msgid "Nuova Stazione"
|
||||||
msgstr "Nuova Stazione"
|
msgstr "Nuova Stazione"
|
||||||
|
|
||||||
# Task Complete Page
|
# Task Complete Page
|
||||||
#: templates/admin/stations.html:49 templates/admin/stations.html:143
|
#: templates/admin/stations.html:49 templates/admin/stations.html:173
|
||||||
#: templates/maker/recipe_editor.html:195
|
#: templates/maker/recipe_editor.html:195
|
||||||
#: templates/measure/task_complete.html:49
|
#: templates/measure/task_complete.html:49
|
||||||
msgid "Codice"
|
msgid "Codice"
|
||||||
msgstr "Codice"
|
msgstr "Codice"
|
||||||
|
|
||||||
#: templates/admin/stations.html:50 templates/admin/stations.html:160
|
#: templates/admin/stations.html:50 templates/admin/stations.html:190
|
||||||
#: templates/maker/recipe_editor.html:213
|
#: templates/maker/recipe_editor.html:213
|
||||||
#: templates/measure/task_complete.html:53
|
#: templates/measure/task_complete.html:53
|
||||||
msgid "Nome"
|
msgid "Nome"
|
||||||
msgstr "Nome"
|
msgstr "Nome"
|
||||||
|
|
||||||
#: templates/admin/stations.html:51 templates/admin/stations.html:169
|
#: templates/admin/stations.html:51 templates/admin/stations.html:199
|
||||||
msgid "Postazione"
|
msgid "Postazione"
|
||||||
msgstr "Postazione"
|
msgstr "Postazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:52 templates/admin/users.html:52
|
#: templates/admin/stations.html:52
|
||||||
|
msgid "Ricette collegate"
|
||||||
|
msgstr "Ricette collegate"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:53 templates/admin/users.html:52
|
||||||
#: templates/measure/task_complete.html:292
|
#: templates/measure/task_complete.html:292
|
||||||
msgid "Stato"
|
msgid "Stato"
|
||||||
msgstr "Stato"
|
msgstr "Stato"
|
||||||
|
|
||||||
#: templates/admin/stations.html:53 templates/admin/users.html:53
|
#: templates/admin/stations.html:54 templates/admin/users.html:53
|
||||||
#: templates/maker/task_editor.html:551
|
#: templates/maker/task_editor.html:551
|
||||||
msgid "Azioni"
|
msgid "Azioni"
|
||||||
msgstr "Azioni"
|
msgstr "Azioni"
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/admin/stations.html:189
|
#: templates/admin/stations.html:68
|
||||||
|
msgid "Nessuna ricetta"
|
||||||
|
msgstr "Nessuna ricetta"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:89 templates/admin/stations.html:219
|
||||||
#: templates/maker/recipe_list.html:186
|
#: templates/maker/recipe_list.html:186
|
||||||
msgid "Attiva"
|
msgid "Attiva"
|
||||||
msgstr "Attiva"
|
msgstr "Attiva"
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/maker/recipe_list.html:186
|
#: templates/admin/stations.html:89 templates/maker/recipe_list.html:186
|
||||||
msgid "Disattivata"
|
msgid "Disattivata"
|
||||||
msgstr "Disattivata"
|
msgstr "Disattivata"
|
||||||
|
|
||||||
# Maker - Recipe List
|
# Maker - Recipe List
|
||||||
#: templates/admin/stations.html:76
|
#: templates/admin/stations.html:95
|
||||||
msgid "Gestisci ricette"
|
msgid "Gestisci ricette"
|
||||||
msgstr "Gestisci ricette"
|
msgstr "Gestisci ricette"
|
||||||
|
|
||||||
#: templates/admin/stations.html:83 templates/admin/users.html:100
|
#: templates/admin/stations.html:105
|
||||||
|
msgid "Azzera le ricette della stazione"
|
||||||
|
msgstr "Azzera le ricette della stazione"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:106
|
||||||
|
msgid "Nessuna ricetta da azzerare"
|
||||||
|
msgstr "Nessuna ricetta da azzerare"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:113 templates/admin/users.html:100
|
||||||
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
||||||
msgid "Modifica"
|
msgid "Modifica"
|
||||||
msgstr "Modifica"
|
msgstr "Modifica"
|
||||||
|
|
||||||
#: templates/admin/stations.html:90 templates/admin/stations.html:359
|
#: templates/admin/stations.html:120 templates/admin/stations.html:421
|
||||||
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
||||||
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
||||||
msgid "Elimina"
|
msgid "Elimina"
|
||||||
msgstr "Elimina"
|
msgstr "Elimina"
|
||||||
|
|
||||||
#: templates/admin/stations.html:108
|
#: templates/admin/stations.html:138
|
||||||
msgid "Nessuna stazione trovata"
|
msgid "Nessuna stazione trovata"
|
||||||
msgstr "Nessuna stazione trovata"
|
msgstr "Nessuna stazione trovata"
|
||||||
|
|
||||||
#: templates/admin/stations.html:114
|
#: templates/admin/stations.html:144
|
||||||
msgid "stazioni"
|
msgid "stazioni"
|
||||||
msgstr "stazioni"
|
msgstr "stazioni"
|
||||||
|
|
||||||
#: templates/admin/stations.html:133
|
#: templates/admin/stations.html:163
|
||||||
msgid "Modifica Stazione"
|
msgid "Modifica Stazione"
|
||||||
msgstr "Modifica Stazione"
|
msgstr "Modifica Stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:152
|
#: templates/admin/stations.html:182
|
||||||
msgid "Il codice non può essere modificato"
|
msgid "Il codice non può essere modificato"
|
||||||
msgstr "Il codice non può essere modificato"
|
msgstr "Il codice non può essere modificato"
|
||||||
|
|
||||||
#: templates/admin/stations.html:155
|
#: templates/admin/stations.html:185
|
||||||
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
||||||
msgstr "Identificativo univoco usato dal client tramite STATION_CODE"
|
msgstr "Identificativo univoco usato dal client tramite STATION_CODE"
|
||||||
|
|
||||||
#: templates/admin/stations.html:164
|
#: templates/admin/stations.html:194
|
||||||
msgid "Nome descrittivo della stazione"
|
msgid "Nome descrittivo della stazione"
|
||||||
msgstr "Nome descrittivo della stazione"
|
msgstr "Nome descrittivo della stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:173
|
#: templates/admin/stations.html:203
|
||||||
msgid "Es. Reparto A - Linea 2"
|
msgid "Es. Reparto A - Linea 2"
|
||||||
msgstr "Es. Reparto A - Linea 2"
|
msgstr "Es. Reparto A - Linea 2"
|
||||||
|
|
||||||
#: templates/admin/stations.html:178
|
#: templates/admin/stations.html:208
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr "Note"
|
msgstr "Note"
|
||||||
|
|
||||||
#: templates/admin/stations.html:182
|
#: templates/admin/stations.html:212
|
||||||
msgid "Note opzionali"
|
msgid "Note opzionali"
|
||||||
msgstr "Note opzionali"
|
msgstr "Note opzionali"
|
||||||
|
|
||||||
#: templates/admin/stations.html:204 templates/admin/stations.html:354
|
#: templates/admin/stations.html:234 templates/admin/stations.html:389
|
||||||
#: templates/admin/users.html:292 templates/admin/users.html:323
|
#: templates/admin/stations.html:416 templates/admin/users.html:292
|
||||||
#: templates/maker/recipe_editor.html:137
|
#: templates/admin/users.html:323 templates/maker/recipe_editor.html:137
|
||||||
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
||||||
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
||||||
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
||||||
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
||||||
#: templates/measure/select_recipe.html:350
|
#: templates/measure/select_recipe.html:367
|
||||||
#: templates/measure/task_execute.html:664
|
#: templates/measure/task_execute.html:684
|
||||||
msgid "Annulla"
|
msgid "Annulla"
|
||||||
msgstr "Annulla"
|
msgstr "Annulla"
|
||||||
|
|
||||||
#: templates/admin/stations.html:210 templates/admin/users.html:298
|
#: templates/admin/stations.html:240 templates/admin/users.html:298
|
||||||
#: templates/auth/profile.html:171
|
#: templates/auth/profile.html:171
|
||||||
msgid "Salva Modifiche"
|
msgid "Salva Modifiche"
|
||||||
msgstr "Salva Modifiche"
|
msgstr "Salva Modifiche"
|
||||||
|
|
||||||
#: templates/admin/stations.html:210
|
#: templates/admin/stations.html:240
|
||||||
msgid "Crea Stazione"
|
msgid "Crea Stazione"
|
||||||
msgstr "Crea Stazione"
|
msgstr "Crea Stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:211 templates/admin/users.html:299
|
#: templates/admin/stations.html:241 templates/admin/users.html:299
|
||||||
#: templates/maker/recipe_editor.html:107
|
#: templates/maker/recipe_editor.html:107
|
||||||
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
||||||
#: templates/measure/task_execute.html:380
|
#: templates/measure/task_execute.html:380
|
||||||
msgid "Salvataggio..."
|
msgid "Salvataggio..."
|
||||||
msgstr "Salvataggio..."
|
msgstr "Salvataggio..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:228
|
#: templates/admin/stations.html:258
|
||||||
msgid "Ricette Assegnate"
|
msgid "Ricette Assegnate"
|
||||||
msgstr "Ricette Assegnate"
|
msgstr "Ricette Assegnate"
|
||||||
|
|
||||||
#: templates/admin/stations.html:246
|
#: templates/admin/stations.html:276
|
||||||
msgid "Filtra per codice o nome ricetta..."
|
msgid "Filtra per codice o nome ricetta..."
|
||||||
msgstr "Filtra per codice o nome ricetta..."
|
msgstr "Filtra per codice o nome ricetta..."
|
||||||
|
|
||||||
#: templates/admin/stations.html:259
|
#: templates/admin/stations.html:289
|
||||||
msgid "Ricette disponibili"
|
msgid "Ricette disponibili"
|
||||||
msgstr "Ricette disponibili"
|
msgstr "Ricette disponibili"
|
||||||
|
|
||||||
#: templates/admin/stations.html:272
|
#: templates/admin/stations.html:302
|
||||||
msgid "Assegna a questa stazione"
|
msgid "Assegna a questa stazione"
|
||||||
msgstr "Assegna a questa stazione"
|
msgstr "Assegna a questa stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:276
|
#: templates/admin/stations.html:306
|
||||||
msgid "Assegna"
|
msgid "Assegna"
|
||||||
msgstr "Assegna"
|
msgstr "Assegna"
|
||||||
|
|
||||||
#: templates/admin/stations.html:293
|
#: templates/admin/stations.html:323
|
||||||
msgid "Assegnate alla stazione"
|
msgid "Assegnate alla stazione"
|
||||||
msgstr "Assegnate alla stazione"
|
msgstr "Assegnate alla stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:305
|
#: templates/admin/stations.html:335
|
||||||
msgid "Rimuovi assegnazione"
|
msgid "Rimuovi assegnazione"
|
||||||
msgstr "Rimuovi assegnazione"
|
msgstr "Rimuovi assegnazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:332
|
#: templates/admin/stations.html:362
|
||||||
#: templates/components/barcode_scanner.html:145
|
#: templates/components/barcode_scanner.html:145
|
||||||
msgid "Chiudi"
|
msgid "Chiudi"
|
||||||
msgstr "Chiudi"
|
msgstr "Chiudi"
|
||||||
|
|
||||||
#: templates/admin/stations.html:345 templates/maker/recipe_list.html:363
|
#: templates/admin/stations.html:376
|
||||||
|
msgid "Azzera Stazione"
|
||||||
|
msgstr "Azzera Stazione"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:378
|
||||||
|
msgid "Rimuovere tutte le ricette assegnate a"
|
||||||
|
msgstr "Rimuovere tutte le ricette assegnate a"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:383
|
||||||
|
msgid "assegnazioni verranno rimosse. La stazione resta, e si potrà riassegnare."
|
||||||
|
msgstr "assegnazioni verranno rimosse. La stazione resta, e si potrà riassegnare."
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:394
|
||||||
|
msgid "Azzera"
|
||||||
|
msgstr "Azzera"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:407 templates/maker/recipe_list.html:363
|
||||||
msgid "Conferma Eliminazione"
|
msgid "Conferma Eliminazione"
|
||||||
msgstr "Conferma Eliminazione"
|
msgstr "Conferma Eliminazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:347
|
#: templates/admin/stations.html:409
|
||||||
msgid "Sei sicuro di voler eliminare la stazione"
|
msgid "Sei sicuro di voler eliminare la stazione"
|
||||||
msgstr "Sei sicuro di voler eliminare la stazione"
|
msgstr "Sei sicuro di voler eliminare la stazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:349
|
#: templates/admin/stations.html:411
|
||||||
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
||||||
msgstr "Verranno rimosse anche tutte le assegnazioni di ricette."
|
msgstr "Verranno rimosse anche tutte le assegnazioni di ricette."
|
||||||
|
|
||||||
#: templates/admin/stations.html:430 templates/admin/stations.html:436
|
#: templates/admin/stations.html:494 templates/admin/stations.html:500
|
||||||
msgid "Nessun risultato per il filtro"
|
msgid "Nessun risultato per il filtro"
|
||||||
msgstr "Nessun risultato per il filtro"
|
msgstr "Nessun risultato per il filtro"
|
||||||
|
|
||||||
#: templates/admin/stations.html:431
|
#: templates/admin/stations.html:495
|
||||||
msgid "Nessuna ricetta nel sistema"
|
msgid "Nessuna ricetta nel sistema"
|
||||||
msgstr "Nessuna ricetta nel sistema"
|
msgstr "Nessuna ricetta nel sistema"
|
||||||
|
|
||||||
#: templates/admin/stations.html:432
|
#: templates/admin/stations.html:496
|
||||||
msgid "Tutte le ricette sono già assegnate"
|
msgid "Tutte le ricette sono già assegnate"
|
||||||
msgstr "Tutte le ricette sono già assegnate"
|
msgstr "Tutte le ricette sono già assegnate"
|
||||||
|
|
||||||
#: templates/admin/stations.html:437
|
#: templates/admin/stations.html:501
|
||||||
msgid "Nessuna ricetta assegnata"
|
msgid "Nessuna ricetta assegnata"
|
||||||
msgstr "Nessuna ricetta assegnata"
|
msgstr "Nessuna ricetta assegnata"
|
||||||
|
|
||||||
#: templates/admin/stations.html:492
|
#: templates/admin/stations.html:558
|
||||||
msgid "Codice e nome sono obbligatori"
|
msgid "Codice e nome sono obbligatori"
|
||||||
msgstr "Codice e nome sono obbligatori"
|
msgstr "Codice e nome sono obbligatori"
|
||||||
|
|
||||||
#: templates/admin/stations.html:509
|
#: templates/admin/stations.html:575
|
||||||
msgid "Errore nella creazione"
|
msgid "Errore nella creazione"
|
||||||
msgstr "Errore nella creazione"
|
msgstr "Errore nella creazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:537
|
#: templates/admin/stations.html:609
|
||||||
|
msgid "Errore nell azzeramento"
|
||||||
|
msgstr "Errore nell azzeramento"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:637
|
||||||
msgid "Errore nella eliminazione"
|
msgid "Errore nella eliminazione"
|
||||||
msgstr "Errore nella eliminazione"
|
msgstr "Errore nella eliminazione"
|
||||||
|
|
||||||
# Maker - API Errors
|
# Maker - API Errors
|
||||||
#: templates/admin/stations.html:562
|
#: templates/admin/stations.html:662
|
||||||
msgid "Errore nel caricamento delle ricette"
|
msgid "Errore nel caricamento delle ricette"
|
||||||
msgstr "Errore nel caricamento delle ricette"
|
msgstr "Errore nel caricamento delle ricette"
|
||||||
|
|
||||||
#: templates/admin/stations.html:590
|
#: templates/admin/stations.html:690
|
||||||
msgid "Errore nella assegnazione"
|
msgid "Errore nella assegnazione"
|
||||||
msgstr "Errore nella assegnazione"
|
msgstr "Errore nella assegnazione"
|
||||||
|
|
||||||
#: templates/admin/stations.html:613
|
#: templates/admin/stations.html:717
|
||||||
msgid "Errore nella rimozione"
|
msgid "Errore nella rimozione"
|
||||||
msgstr "Errore nella rimozione"
|
msgstr "Errore nella rimozione"
|
||||||
|
|
||||||
@@ -457,9 +521,9 @@ msgid "Nuovo Utente"
|
|||||||
msgstr "Nuovo Utente"
|
msgstr "Nuovo Utente"
|
||||||
|
|
||||||
#: templates/admin/users.html:48 templates/admin/users.html:173
|
#: templates/admin/users.html:48 templates/admin/users.html:173
|
||||||
#: templates/admin/users.html:179 templates/auth/login.html:33
|
#: templates/admin/users.html:179 templates/auth/login.html:35
|
||||||
#: templates/auth/login.html:47 templates/auth/profile.html:36
|
#: templates/auth/login.html:49 templates/auth/profile.html:36
|
||||||
#: templates/measure/task_execute.html:649
|
#: templates/measure/task_execute.html:669
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr "Username"
|
msgstr "Username"
|
||||||
|
|
||||||
@@ -510,8 +574,8 @@ msgid "Il nome utente non può essere modificato"
|
|||||||
msgstr "Il nome utente non può essere modificato"
|
msgstr "Il nome utente non può essere modificato"
|
||||||
|
|
||||||
#: templates/admin/users.html:206 templates/admin/users.html:214
|
#: templates/admin/users.html:206 templates/admin/users.html:214
|
||||||
#: templates/auth/login.html:55 templates/auth/login.html:69
|
#: templates/auth/login.html:57 templates/auth/login.html:71
|
||||||
#: templates/measure/task_execute.html:654
|
#: templates/measure/task_execute.html:674
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Password"
|
msgstr "Password"
|
||||||
|
|
||||||
@@ -580,36 +644,36 @@ msgid "Username, nome visualizzato e password sono obbligatori"
|
|||||||
msgstr "Username, nome visualizzato e password sono obbligatori"
|
msgstr "Username, nome visualizzato e password sono obbligatori"
|
||||||
|
|
||||||
# Login Page
|
# Login Page
|
||||||
#: templates/auth/login.html:23
|
#: templates/auth/login.html:25
|
||||||
msgid "Accedi al sistema"
|
msgid "Accedi al sistema"
|
||||||
msgstr "Accedi al sistema"
|
msgstr "Accedi al sistema"
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Nascondi password"
|
msgid "Nascondi password"
|
||||||
msgstr "Nascondi password"
|
msgstr "Nascondi password"
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Mostra password"
|
msgid "Mostra password"
|
||||||
msgstr "Mostra password"
|
msgstr "Mostra password"
|
||||||
|
|
||||||
#: templates/auth/login.html:94
|
#: templates/auth/login.html:96
|
||||||
msgid "Accedi"
|
msgid "Accedi"
|
||||||
msgstr "Accedi"
|
msgstr "Accedi"
|
||||||
|
|
||||||
#: templates/auth/login.html:102
|
#: templates/auth/login.html:104
|
||||||
msgid "Hai dimenticato la password?"
|
msgid "Hai dimenticato la password?"
|
||||||
msgstr "Hai dimenticato la password?"
|
msgstr "Hai dimenticato la password?"
|
||||||
|
|
||||||
#: templates/auth/login.html:104
|
#: templates/auth/login.html:106
|
||||||
msgid "Contatta l'amministratore"
|
msgid "Contatta l'amministratore"
|
||||||
msgstr "Contatta l'amministratore"
|
msgstr "Contatta l'amministratore"
|
||||||
|
|
||||||
# Additional Login Page
|
# Additional Login Page
|
||||||
#: templates/auth/login.html:111
|
#: templates/auth/login.html:113
|
||||||
msgid "Sistema di misurazione industriale"
|
msgid "Sistema di misurazione industriale"
|
||||||
msgstr "Sistema di misurazione industriale"
|
msgstr "Sistema di misurazione industriale"
|
||||||
|
|
||||||
#: templates/auth/profile.html:2 templates/components/navbar.html:147
|
#: templates/auth/profile.html:2 templates/components/navbar.html:149
|
||||||
msgid "Profilo"
|
msgid "Profilo"
|
||||||
msgstr "Profilo"
|
msgstr "Profilo"
|
||||||
|
|
||||||
@@ -659,15 +723,15 @@ msgstr "Esci dal sistema"
|
|||||||
msgid "Termina la sessione corrente e torna alla schermata di login"
|
msgid "Termina la sessione corrente e torna alla schermata di login"
|
||||||
msgstr "Termina la sessione corrente e torna alla schermata di login"
|
msgstr "Termina la sessione corrente e torna alla schermata di login"
|
||||||
|
|
||||||
#: templates/auth/profile.html:197 templates/components/navbar.html:158
|
#: templates/auth/profile.html:197 templates/components/navbar.html:160
|
||||||
#: templates/components/navbar.html:167
|
#: templates/components/navbar.html:169
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Logout"
|
msgstr "Logout"
|
||||||
|
|
||||||
#: templates/components/barcode_scanner.html:21
|
#: templates/components/barcode_scanner.html:21
|
||||||
#: templates/components/barcode_scanner.html:54
|
#: templates/components/barcode_scanner.html:54
|
||||||
#: templates/measure/select_recipe.html:90
|
#: templates/measure/select_recipe.html:107
|
||||||
#: templates/measure/select_recipe.html:302
|
#: templates/measure/select_recipe.html:319
|
||||||
msgid "Scansiona Barcode"
|
msgid "Scansiona Barcode"
|
||||||
msgstr "Scansiona Barcode"
|
msgstr "Scansiona Barcode"
|
||||||
|
|
||||||
@@ -753,7 +817,7 @@ msgstr "Utenti"
|
|||||||
msgid "Stazioni"
|
msgid "Stazioni"
|
||||||
msgstr "Stazioni"
|
msgstr "Stazioni"
|
||||||
|
|
||||||
#: templates/components/navbar.html:154
|
#: templates/components/navbar.html:156
|
||||||
msgid "Logout bloccato durante le misurazioni"
|
msgid "Logout bloccato durante le misurazioni"
|
||||||
msgstr "Logout bloccato durante le misurazioni"
|
msgstr "Logout bloccato durante le misurazioni"
|
||||||
|
|
||||||
@@ -761,11 +825,6 @@ msgstr "Logout bloccato durante le misurazioni"
|
|||||||
msgid "Prossima misura"
|
msgid "Prossima misura"
|
||||||
msgstr "Prossima misura"
|
msgstr "Prossima misura"
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:2
|
|
||||||
#: templates/errors/station_not_configured.html:16
|
|
||||||
msgid "Stazione non configurata"
|
|
||||||
msgstr "Stazione non configurata"
|
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:20
|
#: templates/errors/station_not_configured.html:20
|
||||||
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
||||||
msgstr "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
msgstr "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
||||||
@@ -807,7 +866,7 @@ msgstr "Anteprima"
|
|||||||
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
||||||
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
||||||
#: templates/measure/task_complete.html:168
|
#: templates/measure/task_complete.html:168
|
||||||
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
|
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
|
||||||
#: templates/measure/task_list.html:156
|
#: templates/measure/task_list.html:156
|
||||||
msgid "Task"
|
msgid "Task"
|
||||||
msgstr "Task"
|
msgstr "Task"
|
||||||
@@ -956,7 +1015,8 @@ msgstr "Errore durante eliminazione"
|
|||||||
|
|
||||||
# Recipe Selection Additional
|
# Recipe Selection Additional
|
||||||
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
||||||
#: templates/measure/task_execute.html:1111
|
#: templates/measure/task_execute.html:1057
|
||||||
|
#: templates/measure/task_execute.html:1241
|
||||||
msgid "Errore di connessione"
|
msgid "Errore di connessione"
|
||||||
msgstr "Errore di connessione"
|
msgstr "Errore di connessione"
|
||||||
|
|
||||||
@@ -989,7 +1049,7 @@ msgid "ricetta trovata"
|
|||||||
msgstr "ricetta trovata"
|
msgstr "ricetta trovata"
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:144
|
#: templates/maker/recipe_list.html:144
|
||||||
#: templates/measure/select_recipe.html:158
|
#: templates/measure/select_recipe.html:175
|
||||||
msgid "ricette trovate"
|
msgid "ricette trovate"
|
||||||
msgstr "ricette trovate"
|
msgstr "ricette trovate"
|
||||||
|
|
||||||
@@ -1007,7 +1067,7 @@ msgid "Versioni"
|
|||||||
msgstr "Versioni"
|
msgstr "Versioni"
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:297
|
#: templates/maker/recipe_list.html:297
|
||||||
#: templates/measure/select_recipe.html:256
|
#: templates/measure/select_recipe.html:273
|
||||||
msgid "Nessuna ricetta trovata"
|
msgid "Nessuna ricetta trovata"
|
||||||
msgstr "Nessuna ricetta trovata"
|
msgstr "Nessuna ricetta trovata"
|
||||||
|
|
||||||
@@ -1151,7 +1211,7 @@ msgid "Torna ai Task"
|
|||||||
msgstr "Torna ai Task"
|
msgstr "Torna ai Task"
|
||||||
|
|
||||||
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
||||||
#: templates/measure/select_recipe.html:236
|
#: templates/measure/select_recipe.html:253
|
||||||
msgid "Seleziona"
|
msgid "Seleziona"
|
||||||
msgstr "Seleziona"
|
msgstr "Seleziona"
|
||||||
|
|
||||||
@@ -1554,7 +1614,7 @@ msgid "Misurazione aggiunta"
|
|||||||
msgstr "Misurazione aggiunta"
|
msgstr "Misurazione aggiunta"
|
||||||
|
|
||||||
#: templates/maker/task_editor.html:1645
|
#: templates/maker/task_editor.html:1645
|
||||||
#: templates/measure/task_execute.html:862
|
#: templates/measure/task_execute.html:931
|
||||||
msgid "Errore nel salvataggio della misurazione"
|
msgid "Errore nel salvataggio della misurazione"
|
||||||
msgstr "Errore nel salvataggio della misurazione"
|
msgstr "Errore nel salvataggio della misurazione"
|
||||||
|
|
||||||
@@ -1629,57 +1689,69 @@ msgstr "Scegli la ricetta di misura da eseguire"
|
|||||||
msgid "Stazione"
|
msgid "Stazione"
|
||||||
msgstr "Stazione"
|
msgstr "Stazione"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:102
|
#: templates/measure/select_recipe.html:89
|
||||||
|
msgid "Stazione forzata per collaudo"
|
||||||
|
msgstr "Stazione forzata per collaudo"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:89
|
||||||
|
msgid "configurata"
|
||||||
|
msgstr "configurata"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:94
|
||||||
|
msgid "Ripristina"
|
||||||
|
msgstr "Ripristina"
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:119
|
||||||
msgid "Cerca ricetta"
|
msgid "Cerca ricetta"
|
||||||
msgstr "Cerca ricetta"
|
msgstr "Cerca ricetta"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:106
|
#: templates/measure/select_recipe.html:123
|
||||||
msgid "Nome, codice o descrizione..."
|
msgid "Nome, codice o descrizione..."
|
||||||
msgstr "Nome, codice o descrizione..."
|
msgstr "Nome, codice o descrizione..."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:119
|
#: templates/measure/select_recipe.html:136
|
||||||
msgid "Tracciabilità"
|
msgid "Tracciabilità"
|
||||||
msgstr "Tracciabilità"
|
msgstr "Tracciabilità"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:120
|
#: templates/measure/select_recipe.html:137
|
||||||
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
||||||
msgstr "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
msgstr "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:130
|
#: templates/measure/select_recipe.html:147
|
||||||
#: templates/measure/task_complete.html:281
|
#: templates/measure/task_complete.html:281
|
||||||
msgid "Numero Lotto"
|
msgid "Numero Lotto"
|
||||||
msgstr "Numero Lotto"
|
msgstr "Numero Lotto"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:134
|
#: templates/measure/select_recipe.html:151
|
||||||
msgid "Es. LOT-2026-001 (opzionale)"
|
msgid "Es. LOT-2026-001 (opzionale)"
|
||||||
msgstr "Es. LOT-2026-001 (opzionale)"
|
msgstr "Es. LOT-2026-001 (opzionale)"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:144
|
#: templates/measure/select_recipe.html:161
|
||||||
#: templates/measure/task_complete.html:282
|
#: templates/measure/task_complete.html:282
|
||||||
msgid "Numero Seriale"
|
msgid "Numero Seriale"
|
||||||
msgstr "Numero Seriale"
|
msgstr "Numero Seriale"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:148
|
#: templates/measure/select_recipe.html:165
|
||||||
msgid "Es. SN-000123 (opzionale)"
|
msgid "Es. SN-000123 (opzionale)"
|
||||||
msgstr "Es. SN-000123 (opzionale)"
|
msgstr "Es. SN-000123 (opzionale)"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:201
|
#: templates/measure/select_recipe.html:218
|
||||||
msgid "Nessuna descrizione disponibile"
|
msgid "Nessuna descrizione disponibile"
|
||||||
msgstr "Nessuna descrizione disponibile"
|
msgstr "Nessuna descrizione disponibile"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:260
|
#: templates/measure/select_recipe.html:277
|
||||||
msgid "Nessun risultato per"
|
msgid "Nessun risultato per"
|
||||||
msgstr "Nessun risultato per"
|
msgstr "Nessun risultato per"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:261
|
#: templates/measure/select_recipe.html:278
|
||||||
msgid "Prova con un termine diverso."
|
msgid "Prova con un termine diverso."
|
||||||
msgstr "Prova con un termine diverso."
|
msgstr "Prova con un termine diverso."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:264
|
#: templates/measure/select_recipe.html:281
|
||||||
msgid "Non ci sono ricette disponibili al momento."
|
msgid "Non ci sono ricette disponibili al momento."
|
||||||
msgstr "Non ci sono ricette disponibili al momento."
|
msgstr "Non ci sono ricette disponibili al momento."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:317
|
#: templates/measure/select_recipe.html:334
|
||||||
msgid ""
|
msgid ""
|
||||||
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
||||||
"automaticamente."
|
"automaticamente."
|
||||||
@@ -1687,28 +1759,28 @@ msgstr ""
|
|||||||
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
||||||
"automaticamente."
|
"automaticamente."
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:322
|
#: templates/measure/select_recipe.html:339
|
||||||
msgid "Codice Ricetta"
|
msgid "Codice Ricetta"
|
||||||
msgstr "Codice Ricetta"
|
msgstr "Codice Ricetta"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:328
|
#: templates/measure/select_recipe.html:345
|
||||||
msgid "Es. REC-001"
|
msgid "Es. REC-001"
|
||||||
msgstr "Es. REC-001"
|
msgstr "Es. REC-001"
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:364
|
#: templates/measure/select_recipe.html:381
|
||||||
msgid "Cerca"
|
msgid "Cerca"
|
||||||
msgstr "Cerca"
|
msgstr "Cerca"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:3
|
#: templates/measure/task_complete.html:3
|
||||||
#: templates/measure/task_complete.html:36
|
#: templates/measure/task_complete.html:36
|
||||||
#: templates/measure/task_execute.html:112
|
#: templates/measure/task_execute.html:112
|
||||||
#: templates/measure/task_execute.html:585
|
#: templates/measure/task_execute.html:605
|
||||||
#: templates/statistics/dashboard.html:139
|
#: templates/statistics/dashboard.html:139
|
||||||
msgid "Riepilogo"
|
msgid "Riepilogo"
|
||||||
msgstr "Riepilogo"
|
msgstr "Riepilogo"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:44
|
#: templates/measure/task_complete.html:44
|
||||||
#: templates/measure/task_execute.html:562
|
#: templates/measure/task_execute.html:582
|
||||||
msgid "Misurazioni Complete"
|
msgid "Misurazioni Complete"
|
||||||
msgstr "Misurazioni Complete"
|
msgstr "Misurazioni Complete"
|
||||||
|
|
||||||
@@ -1729,12 +1801,12 @@ msgid "Totale"
|
|||||||
msgstr "Totale"
|
msgstr "Totale"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:103
|
#: templates/measure/task_complete.html:103
|
||||||
#: templates/measure/task_execute.html:570
|
#: templates/measure/task_execute.html:590
|
||||||
msgid "Conformi"
|
msgid "Conformi"
|
||||||
msgstr "Conformi"
|
msgstr "Conformi"
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:120
|
#: templates/measure/task_complete.html:120
|
||||||
#: templates/measure/task_execute.html:574
|
#: templates/measure/task_execute.html:594
|
||||||
msgid "Attenzione"
|
msgid "Attenzione"
|
||||||
msgstr "Attenzione"
|
msgstr "Attenzione"
|
||||||
|
|
||||||
@@ -1934,77 +2006,85 @@ msgstr "Prossima misurazione tra"
|
|||||||
msgid "Ciclo"
|
msgid "Ciclo"
|
||||||
msgstr "Ciclo"
|
msgstr "Ciclo"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:446
|
#: templates/measure/task_execute.html:445
|
||||||
#: templates/measure/task_execute.html:612
|
msgid "Produzione non registrata sul server"
|
||||||
|
msgstr "Produzione non registrata sul server"
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:466
|
||||||
|
#: templates/measure/task_execute.html:632
|
||||||
msgid "Avvio Produzione"
|
msgid "Avvio Produzione"
|
||||||
msgstr "Avvio Produzione"
|
msgstr "Avvio Produzione"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:450
|
#: templates/measure/task_execute.html:470
|
||||||
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
||||||
msgstr "Invia segnale al gestionale per avviare il timer della linea"
|
msgstr "Invia segnale al gestionale per avviare il timer della linea"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:461
|
#: templates/measure/task_execute.html:481
|
||||||
msgid "Produzione avviata"
|
msgid "Produzione avviata"
|
||||||
msgstr "Produzione avviata"
|
msgstr "Produzione avviata"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:506
|
#: templates/measure/task_execute.html:526
|
||||||
msgid "Fine ciclo misura"
|
msgid "Fine ciclo misura"
|
||||||
msgstr "Fine ciclo misura"
|
msgstr "Fine ciclo misura"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:516
|
#: templates/measure/task_execute.html:536
|
||||||
#: templates/measure/task_execute.html:530
|
#: templates/measure/task_execute.html:550
|
||||||
msgid "Completato"
|
msgid "Completato"
|
||||||
msgstr "Completato"
|
msgstr "Completato"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "Tutte le"
|
msgid "Tutte le"
|
||||||
msgstr "Tutte le"
|
msgstr "Tutte le"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "misurazioni sono state registrate."
|
msgid "misurazioni sono state registrate."
|
||||||
msgstr "misurazioni sono state registrate."
|
msgstr "misurazioni sono state registrate."
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:578
|
#: templates/measure/task_execute.html:598
|
||||||
msgid "Non Conf."
|
msgid "Non Conf."
|
||||||
msgstr "Non Conf."
|
msgstr "Non Conf."
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:592
|
#: templates/measure/task_execute.html:612
|
||||||
msgid "Conferma ciclo"
|
msgid "Conferma ciclo"
|
||||||
msgstr "Conferma ciclo"
|
msgstr "Conferma ciclo"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:602
|
#: templates/measure/task_execute.html:622
|
||||||
msgid "Task successivo"
|
msgid "Task successivo"
|
||||||
msgstr "Task successivo"
|
msgstr "Task successivo"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:642
|
#: templates/measure/task_execute.html:662
|
||||||
msgid "Autorizzazione capoturno"
|
msgid "Autorizzazione capoturno"
|
||||||
msgstr "Autorizzazione capoturno"
|
msgstr "Autorizzazione capoturno"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:651
|
#: templates/measure/task_execute.html:671
|
||||||
msgid "Username capoturno"
|
msgid "Username capoturno"
|
||||||
msgstr "Username capoturno"
|
msgstr "Username capoturno"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:673
|
#: templates/measure/task_execute.html:693
|
||||||
msgid "Autorizza"
|
msgid "Autorizza"
|
||||||
msgstr "Autorizza"
|
msgstr "Autorizza"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:906
|
#: templates/measure/task_execute.html:975
|
||||||
msgid "Errore di rete. Riprovare."
|
msgid "Errore di rete. Riprovare."
|
||||||
msgstr "Errore di rete. Riprovare."
|
msgstr "Errore di rete. Riprovare."
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1068
|
#: templates/measure/task_execute.html:1052
|
||||||
|
msgid "Errore di comunicazione con il server"
|
||||||
|
msgstr "Errore di comunicazione con il server"
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:1198
|
||||||
msgid "Misurazione fuori tolleranza"
|
msgid "Misurazione fuori tolleranza"
|
||||||
msgstr "Misurazione fuori tolleranza"
|
msgstr "Misurazione fuori tolleranza"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1069
|
#: templates/measure/task_execute.html:1199
|
||||||
msgid "Fermo linea richiesto"
|
msgid "Fermo linea richiesto"
|
||||||
msgstr "Fermo linea richiesto"
|
msgstr "Fermo linea richiesto"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1070
|
#: templates/measure/task_execute.html:1200
|
||||||
msgid "Fine produzione richiesta"
|
msgid "Fine produzione richiesta"
|
||||||
msgstr "Fine produzione richiesta"
|
msgstr "Fine produzione richiesta"
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1089
|
#: templates/measure/task_execute.html:1219
|
||||||
msgid "Credenziali non valide o utente non autorizzato"
|
msgid "Credenziali non valide o utente non autorizzato"
|
||||||
msgstr "Credenziali non valide o utente non autorizzato"
|
msgstr "Credenziali non valide o utente non autorizzato"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-06-11 15:33+0000\n"
|
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -39,7 +39,7 @@ msgstr ""
|
|||||||
msgid "Inserisci username e password"
|
msgid "Inserisci username e password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/auth.py:81 blueprints/measure.py:346
|
#: blueprints/auth.py:81 blueprints/measure.py:407
|
||||||
msgid "Credenziali non valide"
|
msgid "Credenziali non valide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -99,64 +99,92 @@ msgstr ""
|
|||||||
msgid "Nome file vuoto"
|
msgid "Nome file vuoto"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:38
|
#: blueprints/measure.py:45
|
||||||
#, python-format
|
msgid "Il cambio stazione da URL non è abilitato su questa postazione."
|
||||||
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:86 blueprints/measure.py:172
|
#: blueprints/measure.py:50
|
||||||
|
msgid "Stazione riportata a quella configurata."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blueprints/measure.py:56
|
||||||
|
msgid "Impossibile contattare il server per cambiare stazione."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blueprints/measure.py:61
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ricetta non trovata: %(detail)s"
|
msgid "Stazione '%(code)s' inesistente o disattivata: stazione invariata."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blueprints/measure.py:68
|
||||||
|
#, python-format
|
||||||
|
msgid "Stazione corrente: %(code)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:96
|
#: blueprints/measure.py:96
|
||||||
#, python-format
|
#, python-format
|
||||||
|
msgid "Errore nel caricamento delle ricette: %(detail)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blueprints/measure.py:146 blueprints/measure.py:232
|
||||||
|
#, python-format
|
||||||
|
msgid "Ricetta non trovata: %(detail)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blueprints/measure.py:156
|
||||||
|
#, python-format
|
||||||
msgid "Errore nel caricamento dei task: %(detail)s"
|
msgid "Errore nel caricamento dei task: %(detail)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:125
|
#: blueprints/measure.py:185
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Task non trovato: %(detail)s"
|
msgid "Task non trovato: %(detail)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:241
|
#: blueprints/measure.py:301
|
||||||
msgid "Codice non fornito"
|
msgid "Codice non fornito"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:247 templates/measure/select_recipe.html:48
|
#: blueprints/measure.py:307 templates/measure/select_recipe.html:48
|
||||||
msgid "Ricetta non trovata"
|
msgid "Ricetta non trovata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:303
|
#: blueprints/measure.py:363
|
||||||
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
msgid "Dati mancanti: subtask_id, version_id e value sono obbligatori"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:322 templates/admin/stations.html:485
|
#: blueprints/measure.py:383 templates/admin/stations.html:549
|
||||||
#: templates/maker/recipe_editor.html:543
|
#: templates/maker/recipe_editor.html:543
|
||||||
msgid "Errore nel salvataggio"
|
msgid "Errore nel salvataggio"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:341
|
#: blueprints/measure.py:402
|
||||||
msgid "Username e password richiesti"
|
msgid "Username e password richiesti"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: blueprints/measure.py:351
|
#: blueprints/measure.py:412
|
||||||
msgid "Utente non autorizzato (richiesto capoturno)"
|
msgid "Utente non autorizzato (richiesto capoturno)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:175
|
#: blueprints/measure.py:431 blueprints/measure.py:448
|
||||||
|
#: templates/errors/station_not_configured.html:2
|
||||||
|
#: templates/errors/station_not_configured.html:16
|
||||||
|
msgid "Stazione non configurata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/base.html:173
|
||||||
msgid "Sessione in scadenza"
|
msgid "Sessione in scadenza"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "Sarai disconnesso tra"
|
msgid "Sarai disconnesso tra"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:178
|
#: templates/base.html:176
|
||||||
msgid "secondi per inattività."
|
msgid "secondi per inattività."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:181
|
#: templates/base.html:179
|
||||||
msgid "Continua a lavorare"
|
msgid "Continua a lavorare"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -198,16 +226,16 @@ msgstr ""
|
|||||||
msgid "Salva impostazioni"
|
msgid "Salva impostazioni"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/settings.html:107 templates/admin/stations.html:516
|
#: templates/admin/settings.html:107 templates/admin/stations.html:582
|
||||||
#: templates/admin/stations.html:544 templates/admin/stations.html:565
|
#: templates/admin/stations.html:621 templates/admin/stations.html:644
|
||||||
#: templates/admin/stations.html:596 templates/admin/stations.html:618
|
#: templates/admin/stations.html:665 templates/admin/stations.html:700
|
||||||
#: templates/admin/users.html:497 templates/admin/users.html:533
|
#: templates/admin/stations.html:723 templates/admin/users.html:497
|
||||||
#: templates/maker/recipe_editor.html:571 templates/maker/task_drawing.html:65
|
#: templates/admin/users.html:533 templates/maker/recipe_editor.html:571
|
||||||
#: templates/maker/task_editor.html:1243 templates/maker/task_editor.html:1321
|
#: templates/maker/task_drawing.html:65 templates/maker/task_editor.html:1243
|
||||||
#: templates/maker/task_editor.html:1382 templates/maker/task_editor.html:1415
|
#: templates/maker/task_editor.html:1321 templates/maker/task_editor.html:1382
|
||||||
#: templates/maker/task_editor.html:1510 templates/maker/task_editor.html:1586
|
#: templates/maker/task_editor.html:1415 templates/maker/task_editor.html:1510
|
||||||
#: templates/maker/task_editor.html:1660 templates/maker/task_editor.html:1697
|
#: templates/maker/task_editor.html:1586 templates/maker/task_editor.html:1660
|
||||||
#: templates/maker/task_editor.html:1776
|
#: templates/maker/task_editor.html:1697 templates/maker/task_editor.html:1776
|
||||||
#: templates/statistics/dashboard.html:372
|
#: templates/statistics/dashboard.html:372
|
||||||
#: templates/statistics/dashboard.html:457
|
#: templates/statistics/dashboard.html:457
|
||||||
msgid "Errore di connessione al server"
|
msgid "Errore di connessione al server"
|
||||||
@@ -227,206 +255,242 @@ msgstr ""
|
|||||||
msgid "Cerca stazione..."
|
msgid "Cerca stazione..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:39 templates/admin/stations.html:133
|
#: templates/admin/stations.html:39 templates/admin/stations.html:163
|
||||||
msgid "Nuova Stazione"
|
msgid "Nuova Stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:49 templates/admin/stations.html:143
|
#: templates/admin/stations.html:49 templates/admin/stations.html:173
|
||||||
#: templates/maker/recipe_editor.html:195
|
#: templates/maker/recipe_editor.html:195
|
||||||
#: templates/measure/task_complete.html:49
|
#: templates/measure/task_complete.html:49
|
||||||
msgid "Codice"
|
msgid "Codice"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:50 templates/admin/stations.html:160
|
#: templates/admin/stations.html:50 templates/admin/stations.html:190
|
||||||
#: templates/maker/recipe_editor.html:213
|
#: templates/maker/recipe_editor.html:213
|
||||||
#: templates/measure/task_complete.html:53
|
#: templates/measure/task_complete.html:53
|
||||||
msgid "Nome"
|
msgid "Nome"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:51 templates/admin/stations.html:169
|
#: templates/admin/stations.html:51 templates/admin/stations.html:199
|
||||||
msgid "Postazione"
|
msgid "Postazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:52 templates/admin/users.html:52
|
#: templates/admin/stations.html:52
|
||||||
|
msgid "Ricette collegate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:53 templates/admin/users.html:52
|
||||||
#: templates/measure/task_complete.html:292
|
#: templates/measure/task_complete.html:292
|
||||||
msgid "Stato"
|
msgid "Stato"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:53 templates/admin/users.html:53
|
#: templates/admin/stations.html:54 templates/admin/users.html:53
|
||||||
#: templates/maker/task_editor.html:551
|
#: templates/maker/task_editor.html:551
|
||||||
msgid "Azioni"
|
msgid "Azioni"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/admin/stations.html:189
|
#: templates/admin/stations.html:68
|
||||||
|
msgid "Nessuna ricetta"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:89 templates/admin/stations.html:219
|
||||||
#: templates/maker/recipe_list.html:186
|
#: templates/maker/recipe_list.html:186
|
||||||
msgid "Attiva"
|
msgid "Attiva"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:70 templates/maker/recipe_list.html:186
|
#: templates/admin/stations.html:89 templates/maker/recipe_list.html:186
|
||||||
msgid "Disattivata"
|
msgid "Disattivata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:76
|
#: templates/admin/stations.html:95
|
||||||
msgid "Gestisci ricette"
|
msgid "Gestisci ricette"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:83 templates/admin/users.html:100
|
#: templates/admin/stations.html:105
|
||||||
|
msgid "Azzera le ricette della stazione"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:106
|
||||||
|
msgid "Nessuna ricetta da azzerare"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:113 templates/admin/users.html:100
|
||||||
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
#: templates/maker/recipe_list.html:237 templates/maker/task_editor.html:602
|
||||||
msgid "Modifica"
|
msgid "Modifica"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:90 templates/admin/stations.html:359
|
#: templates/admin/stations.html:120 templates/admin/stations.html:421
|
||||||
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
#: templates/maker/recipe_list.html:277 templates/maker/recipe_list.html:387
|
||||||
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
#: templates/maker/task_drawing.html:275 templates/maker/task_editor.html:610
|
||||||
msgid "Elimina"
|
msgid "Elimina"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:108
|
#: templates/admin/stations.html:138
|
||||||
msgid "Nessuna stazione trovata"
|
msgid "Nessuna stazione trovata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:114
|
#: templates/admin/stations.html:144
|
||||||
msgid "stazioni"
|
msgid "stazioni"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:133
|
#: templates/admin/stations.html:163
|
||||||
msgid "Modifica Stazione"
|
msgid "Modifica Stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:152
|
#: templates/admin/stations.html:182
|
||||||
msgid "Il codice non può essere modificato"
|
msgid "Il codice non può essere modificato"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:155
|
#: templates/admin/stations.html:185
|
||||||
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
msgid "Identificativo univoco usato dal client tramite STATION_CODE"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:164
|
#: templates/admin/stations.html:194
|
||||||
msgid "Nome descrittivo della stazione"
|
msgid "Nome descrittivo della stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:173
|
#: templates/admin/stations.html:203
|
||||||
msgid "Es. Reparto A - Linea 2"
|
msgid "Es. Reparto A - Linea 2"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:178
|
#: templates/admin/stations.html:208
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:182
|
#: templates/admin/stations.html:212
|
||||||
msgid "Note opzionali"
|
msgid "Note opzionali"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:204 templates/admin/stations.html:354
|
#: templates/admin/stations.html:234 templates/admin/stations.html:389
|
||||||
#: templates/admin/users.html:292 templates/admin/users.html:323
|
#: templates/admin/stations.html:416 templates/admin/users.html:292
|
||||||
#: templates/maker/recipe_editor.html:137
|
#: templates/admin/users.html:323 templates/maker/recipe_editor.html:137
|
||||||
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
#: templates/maker/recipe_editor.html:456 templates/maker/recipe_list.html:383
|
||||||
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
#: templates/maker/task_editor.html:294 templates/maker/task_editor.html:403
|
||||||
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
|
||||||
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
|
||||||
#: templates/measure/select_recipe.html:350
|
#: templates/measure/select_recipe.html:367
|
||||||
#: templates/measure/task_execute.html:664
|
#: templates/measure/task_execute.html:684
|
||||||
msgid "Annulla"
|
msgid "Annulla"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:210 templates/admin/users.html:298
|
#: templates/admin/stations.html:240 templates/admin/users.html:298
|
||||||
#: templates/auth/profile.html:171
|
#: templates/auth/profile.html:171
|
||||||
msgid "Salva Modifiche"
|
msgid "Salva Modifiche"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:210
|
#: templates/admin/stations.html:240
|
||||||
msgid "Crea Stazione"
|
msgid "Crea Stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:211 templates/admin/users.html:299
|
#: templates/admin/stations.html:241 templates/admin/users.html:299
|
||||||
#: templates/maker/recipe_editor.html:107
|
#: templates/maker/recipe_editor.html:107
|
||||||
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
#: templates/maker/recipe_editor.html:448 templates/maker/task_drawing.html:61
|
||||||
#: templates/measure/task_execute.html:380
|
#: templates/measure/task_execute.html:380
|
||||||
msgid "Salvataggio..."
|
msgid "Salvataggio..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:228
|
#: templates/admin/stations.html:258
|
||||||
msgid "Ricette Assegnate"
|
msgid "Ricette Assegnate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:246
|
#: templates/admin/stations.html:276
|
||||||
msgid "Filtra per codice o nome ricetta..."
|
msgid "Filtra per codice o nome ricetta..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:259
|
#: templates/admin/stations.html:289
|
||||||
msgid "Ricette disponibili"
|
msgid "Ricette disponibili"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:272
|
#: templates/admin/stations.html:302
|
||||||
msgid "Assegna a questa stazione"
|
msgid "Assegna a questa stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:276
|
#: templates/admin/stations.html:306
|
||||||
msgid "Assegna"
|
msgid "Assegna"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:293
|
#: templates/admin/stations.html:323
|
||||||
msgid "Assegnate alla stazione"
|
msgid "Assegnate alla stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:305
|
#: templates/admin/stations.html:335
|
||||||
msgid "Rimuovi assegnazione"
|
msgid "Rimuovi assegnazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:332
|
#: templates/admin/stations.html:362
|
||||||
#: templates/components/barcode_scanner.html:145
|
#: templates/components/barcode_scanner.html:145
|
||||||
msgid "Chiudi"
|
msgid "Chiudi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:345 templates/maker/recipe_list.html:363
|
#: templates/admin/stations.html:376
|
||||||
|
msgid "Azzera Stazione"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:378
|
||||||
|
msgid "Rimuovere tutte le ricette assegnate a"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:383
|
||||||
|
msgid "assegnazioni verranno rimosse. La stazione resta, e si potrà riassegnare."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:394
|
||||||
|
msgid "Azzera"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:407 templates/maker/recipe_list.html:363
|
||||||
msgid "Conferma Eliminazione"
|
msgid "Conferma Eliminazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:347
|
#: templates/admin/stations.html:409
|
||||||
msgid "Sei sicuro di voler eliminare la stazione"
|
msgid "Sei sicuro di voler eliminare la stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:349
|
#: templates/admin/stations.html:411
|
||||||
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
msgid "Verranno rimosse anche tutte le assegnazioni di ricette."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:430 templates/admin/stations.html:436
|
#: templates/admin/stations.html:494 templates/admin/stations.html:500
|
||||||
msgid "Nessun risultato per il filtro"
|
msgid "Nessun risultato per il filtro"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:431
|
#: templates/admin/stations.html:495
|
||||||
msgid "Nessuna ricetta nel sistema"
|
msgid "Nessuna ricetta nel sistema"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:432
|
#: templates/admin/stations.html:496
|
||||||
msgid "Tutte le ricette sono già assegnate"
|
msgid "Tutte le ricette sono già assegnate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:437
|
#: templates/admin/stations.html:501
|
||||||
msgid "Nessuna ricetta assegnata"
|
msgid "Nessuna ricetta assegnata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:492
|
#: templates/admin/stations.html:558
|
||||||
msgid "Codice e nome sono obbligatori"
|
msgid "Codice e nome sono obbligatori"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:509
|
#: templates/admin/stations.html:575
|
||||||
msgid "Errore nella creazione"
|
msgid "Errore nella creazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:537
|
#: templates/admin/stations.html:609
|
||||||
|
msgid "Errore nell azzeramento"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/stations.html:637
|
||||||
msgid "Errore nella eliminazione"
|
msgid "Errore nella eliminazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:562
|
#: templates/admin/stations.html:662
|
||||||
msgid "Errore nel caricamento delle ricette"
|
msgid "Errore nel caricamento delle ricette"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:590
|
#: templates/admin/stations.html:690
|
||||||
msgid "Errore nella assegnazione"
|
msgid "Errore nella assegnazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/stations.html:613
|
#: templates/admin/stations.html:717
|
||||||
msgid "Errore nella rimozione"
|
msgid "Errore nella rimozione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -447,9 +511,9 @@ msgid "Nuovo Utente"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/users.html:48 templates/admin/users.html:173
|
#: templates/admin/users.html:48 templates/admin/users.html:173
|
||||||
#: templates/admin/users.html:179 templates/auth/login.html:33
|
#: templates/admin/users.html:179 templates/auth/login.html:35
|
||||||
#: templates/auth/login.html:47 templates/auth/profile.html:36
|
#: templates/auth/login.html:49 templates/auth/profile.html:36
|
||||||
#: templates/measure/task_execute.html:649
|
#: templates/measure/task_execute.html:669
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -500,8 +564,8 @@ msgid "Il nome utente non può essere modificato"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/users.html:206 templates/admin/users.html:214
|
#: templates/admin/users.html:206 templates/admin/users.html:214
|
||||||
#: templates/auth/login.html:55 templates/auth/login.html:69
|
#: templates/auth/login.html:57 templates/auth/login.html:71
|
||||||
#: templates/measure/task_execute.html:654
|
#: templates/measure/task_execute.html:674
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -569,35 +633,35 @@ msgstr ""
|
|||||||
msgid "Username, nome visualizzato e password sono obbligatori"
|
msgid "Username, nome visualizzato e password sono obbligatori"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:23
|
#: templates/auth/login.html:25
|
||||||
msgid "Accedi al sistema"
|
msgid "Accedi al sistema"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Nascondi password"
|
msgid "Nascondi password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:73
|
#: templates/auth/login.html:75
|
||||||
msgid "Mostra password"
|
msgid "Mostra password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:94
|
#: templates/auth/login.html:96
|
||||||
msgid "Accedi"
|
msgid "Accedi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:102
|
#: templates/auth/login.html:104
|
||||||
msgid "Hai dimenticato la password?"
|
msgid "Hai dimenticato la password?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:104
|
#: templates/auth/login.html:106
|
||||||
msgid "Contatta l'amministratore"
|
msgid "Contatta l'amministratore"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/login.html:111
|
#: templates/auth/login.html:113
|
||||||
msgid "Sistema di misurazione industriale"
|
msgid "Sistema di misurazione industriale"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/profile.html:2 templates/components/navbar.html:147
|
#: templates/auth/profile.html:2 templates/components/navbar.html:149
|
||||||
msgid "Profilo"
|
msgid "Profilo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -645,15 +709,15 @@ msgstr ""
|
|||||||
msgid "Termina la sessione corrente e torna alla schermata di login"
|
msgid "Termina la sessione corrente e torna alla schermata di login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/auth/profile.html:197 templates/components/navbar.html:158
|
#: templates/auth/profile.html:197 templates/components/navbar.html:160
|
||||||
#: templates/components/navbar.html:167
|
#: templates/components/navbar.html:169
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/components/barcode_scanner.html:21
|
#: templates/components/barcode_scanner.html:21
|
||||||
#: templates/components/barcode_scanner.html:54
|
#: templates/components/barcode_scanner.html:54
|
||||||
#: templates/measure/select_recipe.html:90
|
#: templates/measure/select_recipe.html:107
|
||||||
#: templates/measure/select_recipe.html:302
|
#: templates/measure/select_recipe.html:319
|
||||||
msgid "Scansiona Barcode"
|
msgid "Scansiona Barcode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -734,7 +798,7 @@ msgstr ""
|
|||||||
msgid "Stazioni"
|
msgid "Stazioni"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/components/navbar.html:154
|
#: templates/components/navbar.html:156
|
||||||
msgid "Logout bloccato durante le misurazioni"
|
msgid "Logout bloccato durante le misurazioni"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -742,11 +806,6 @@ msgstr ""
|
|||||||
msgid "Prossima misura"
|
msgid "Prossima misura"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:2
|
|
||||||
#: templates/errors/station_not_configured.html:16
|
|
||||||
msgid "Stazione non configurata"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/errors/station_not_configured.html:20
|
#: templates/errors/station_not_configured.html:20
|
||||||
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -784,7 +843,7 @@ msgstr ""
|
|||||||
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
|
||||||
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
|
||||||
#: templates/measure/task_complete.html:168
|
#: templates/measure/task_complete.html:168
|
||||||
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
|
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
|
||||||
#: templates/measure/task_list.html:156
|
#: templates/measure/task_list.html:156
|
||||||
msgid "Task"
|
msgid "Task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -931,7 +990,8 @@ msgid "Errore durante eliminazione"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
|
||||||
#: templates/measure/task_execute.html:1111
|
#: templates/measure/task_execute.html:1057
|
||||||
|
#: templates/measure/task_execute.html:1241
|
||||||
msgid "Errore di connessione"
|
msgid "Errore di connessione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -964,7 +1024,7 @@ msgid "ricetta trovata"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:144
|
#: templates/maker/recipe_list.html:144
|
||||||
#: templates/measure/select_recipe.html:158
|
#: templates/measure/select_recipe.html:175
|
||||||
msgid "ricette trovate"
|
msgid "ricette trovate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -982,7 +1042,7 @@ msgid "Versioni"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/maker/recipe_list.html:297
|
#: templates/maker/recipe_list.html:297
|
||||||
#: templates/measure/select_recipe.html:256
|
#: templates/measure/select_recipe.html:273
|
||||||
msgid "Nessuna ricetta trovata"
|
msgid "Nessuna ricetta trovata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1121,7 +1181,7 @@ msgid "Torna ai Task"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
|
||||||
#: templates/measure/select_recipe.html:236
|
#: templates/measure/select_recipe.html:253
|
||||||
msgid "Seleziona"
|
msgid "Seleziona"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1522,7 +1582,7 @@ msgid "Misurazione aggiunta"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/maker/task_editor.html:1645
|
#: templates/maker/task_editor.html:1645
|
||||||
#: templates/measure/task_execute.html:862
|
#: templates/measure/task_execute.html:931
|
||||||
msgid "Errore nel salvataggio della misurazione"
|
msgid "Errore nel salvataggio della misurazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1593,84 +1653,96 @@ msgstr ""
|
|||||||
msgid "Stazione"
|
msgid "Stazione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:102
|
#: templates/measure/select_recipe.html:89
|
||||||
msgid "Cerca ricetta"
|
msgid "Stazione forzata per collaudo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:106
|
#: templates/measure/select_recipe.html:89
|
||||||
msgid "Nome, codice o descrizione..."
|
msgid "configurata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:94
|
||||||
|
msgid "Ripristina"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:119
|
#: templates/measure/select_recipe.html:119
|
||||||
|
msgid "Cerca ricetta"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:123
|
||||||
|
msgid "Nome, codice o descrizione..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/measure/select_recipe.html:136
|
||||||
msgid "Tracciabilità"
|
msgid "Tracciabilità"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:120
|
#: templates/measure/select_recipe.html:137
|
||||||
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:130
|
#: templates/measure/select_recipe.html:147
|
||||||
#: templates/measure/task_complete.html:281
|
#: templates/measure/task_complete.html:281
|
||||||
msgid "Numero Lotto"
|
msgid "Numero Lotto"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:134
|
#: templates/measure/select_recipe.html:151
|
||||||
msgid "Es. LOT-2026-001 (opzionale)"
|
msgid "Es. LOT-2026-001 (opzionale)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:144
|
#: templates/measure/select_recipe.html:161
|
||||||
#: templates/measure/task_complete.html:282
|
#: templates/measure/task_complete.html:282
|
||||||
msgid "Numero Seriale"
|
msgid "Numero Seriale"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:148
|
#: templates/measure/select_recipe.html:165
|
||||||
msgid "Es. SN-000123 (opzionale)"
|
msgid "Es. SN-000123 (opzionale)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:201
|
#: templates/measure/select_recipe.html:218
|
||||||
msgid "Nessuna descrizione disponibile"
|
msgid "Nessuna descrizione disponibile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:260
|
#: templates/measure/select_recipe.html:277
|
||||||
msgid "Nessun risultato per"
|
msgid "Nessun risultato per"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:261
|
#: templates/measure/select_recipe.html:278
|
||||||
msgid "Prova con un termine diverso."
|
msgid "Prova con un termine diverso."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:264
|
#: templates/measure/select_recipe.html:281
|
||||||
msgid "Non ci sono ricette disponibili al momento."
|
msgid "Non ci sono ricette disponibili al momento."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:317
|
#: templates/measure/select_recipe.html:334
|
||||||
msgid ""
|
msgid ""
|
||||||
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
"Inserisci o scansiona il codice della ricetta per selezionarla "
|
||||||
"automaticamente."
|
"automaticamente."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:322
|
#: templates/measure/select_recipe.html:339
|
||||||
msgid "Codice Ricetta"
|
msgid "Codice Ricetta"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:328
|
#: templates/measure/select_recipe.html:345
|
||||||
msgid "Es. REC-001"
|
msgid "Es. REC-001"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/select_recipe.html:364
|
#: templates/measure/select_recipe.html:381
|
||||||
msgid "Cerca"
|
msgid "Cerca"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:3
|
#: templates/measure/task_complete.html:3
|
||||||
#: templates/measure/task_complete.html:36
|
#: templates/measure/task_complete.html:36
|
||||||
#: templates/measure/task_execute.html:112
|
#: templates/measure/task_execute.html:112
|
||||||
#: templates/measure/task_execute.html:585
|
#: templates/measure/task_execute.html:605
|
||||||
#: templates/statistics/dashboard.html:139
|
#: templates/statistics/dashboard.html:139
|
||||||
msgid "Riepilogo"
|
msgid "Riepilogo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:44
|
#: templates/measure/task_complete.html:44
|
||||||
#: templates/measure/task_execute.html:562
|
#: templates/measure/task_execute.html:582
|
||||||
msgid "Misurazioni Complete"
|
msgid "Misurazioni Complete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1690,12 +1762,12 @@ msgid "Totale"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:103
|
#: templates/measure/task_complete.html:103
|
||||||
#: templates/measure/task_execute.html:570
|
#: templates/measure/task_execute.html:590
|
||||||
msgid "Conformi"
|
msgid "Conformi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_complete.html:120
|
#: templates/measure/task_complete.html:120
|
||||||
#: templates/measure/task_execute.html:574
|
#: templates/measure/task_execute.html:594
|
||||||
msgid "Attenzione"
|
msgid "Attenzione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1893,77 +1965,85 @@ msgstr ""
|
|||||||
msgid "Ciclo"
|
msgid "Ciclo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:446
|
#: templates/measure/task_execute.html:445
|
||||||
#: templates/measure/task_execute.html:612
|
msgid "Produzione non registrata sul server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:466
|
||||||
|
#: templates/measure/task_execute.html:632
|
||||||
msgid "Avvio Produzione"
|
msgid "Avvio Produzione"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:450
|
#: templates/measure/task_execute.html:470
|
||||||
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
msgid "Invia segnale al gestionale per avviare il timer della linea"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:461
|
#: templates/measure/task_execute.html:481
|
||||||
msgid "Produzione avviata"
|
msgid "Produzione avviata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:506
|
#: templates/measure/task_execute.html:526
|
||||||
msgid "Fine ciclo misura"
|
msgid "Fine ciclo misura"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:516
|
#: templates/measure/task_execute.html:536
|
||||||
#: templates/measure/task_execute.html:530
|
#: templates/measure/task_execute.html:550
|
||||||
msgid "Completato"
|
msgid "Completato"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "Tutte le"
|
msgid "Tutte le"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:564
|
#: templates/measure/task_execute.html:584
|
||||||
msgid "misurazioni sono state registrate."
|
msgid "misurazioni sono state registrate."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:578
|
#: templates/measure/task_execute.html:598
|
||||||
msgid "Non Conf."
|
msgid "Non Conf."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:592
|
#: templates/measure/task_execute.html:612
|
||||||
msgid "Conferma ciclo"
|
msgid "Conferma ciclo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:602
|
#: templates/measure/task_execute.html:622
|
||||||
msgid "Task successivo"
|
msgid "Task successivo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:642
|
#: templates/measure/task_execute.html:662
|
||||||
msgid "Autorizzazione capoturno"
|
msgid "Autorizzazione capoturno"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:651
|
#: templates/measure/task_execute.html:671
|
||||||
msgid "Username capoturno"
|
msgid "Username capoturno"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:673
|
#: templates/measure/task_execute.html:693
|
||||||
msgid "Autorizza"
|
msgid "Autorizza"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:906
|
#: templates/measure/task_execute.html:975
|
||||||
msgid "Errore di rete. Riprovare."
|
msgid "Errore di rete. Riprovare."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1068
|
#: templates/measure/task_execute.html:1052
|
||||||
|
msgid "Errore di comunicazione con il server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/measure/task_execute.html:1198
|
||||||
msgid "Misurazione fuori tolleranza"
|
msgid "Misurazione fuori tolleranza"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1069
|
#: templates/measure/task_execute.html:1199
|
||||||
msgid "Fermo linea richiesto"
|
msgid "Fermo linea richiesto"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1070
|
#: templates/measure/task_execute.html:1200
|
||||||
msgid "Fine produzione richiesta"
|
msgid "Fine produzione richiesta"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/measure/task_execute.html:1089
|
#: templates/measure/task_execute.html:1219
|
||||||
msgid "Credenziali non valide o utente non autorizzato"
|
msgid "Credenziali non valide o utente non autorizzato"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user