Compare commits

..

1 Commits

Author SHA1 Message Date
a60610c03e Freeze 2.0 warehouse monolista e documento fisso F1/F2 2026-10-02 10:09:44 +02:00
10 changed files with 1019 additions and 20 deletions

View File

@@ -0,0 +1,70 @@
# Warehouse monolista - freeze 2.0
Data: 02/10/2026.
Titolo release: warehouse monolista.
Tag Git: `warehouse-monolista-2.0`.
## Scopo e contenuto
Punto di ripristino prima dello sviluppo del prelievo guidato di due liste.
La versione conserva la prenotazione singola e i cicli F1/F2 separati.
Include i sorgenti e gli script SQL gia' versionati, oltre alla correzione
del documento fisso F1/F2, ai suoi test e alla relativa documentazione.
La denominazione 2.0 identifica questo freeze, non una rinumerazione di tutti
i moduli. Il barcode conserva la versione visibile 1.0.27; repository 1.0.14,
servizio 1.0.25. Il registro delle versioni resta `version_info.py`.
La specifica `specifica_prelievo_due_pickinglist.md` descrive lo sviluppo
successivo, NON una funzionalita' gia' implementata in questa release.
Comprende il vincolo di due liste nella stessa area operativa (5 oppure 6).
## Verifiche del freeze
Gli 11 test di `tests/test_barcode_document_binding.py` sono passati con:
```bat
python -B -m unittest discover -s tests -v
```
Si tratta di test automatici con repository simulato, non di un collaudo
completo della GUI o del terminalino fisico. Non eseguono movimenti sul DB.
## Database e file locali
La correzione F1/F2 non richiede nuovi oggetti SQL. Le patch gia' necessarie
alla versione precedente restano nel repository, inclusa
`apply_online_python_wms_full_patch.sql`. Questo freeze non esegue patch SQL
e non costituisce un backup dei database Mediseawall e SAM.
Gli script in `sql_tests/` servono soltanto alla preparazione locale dei test:
NON lanciarli in produzione. Il ripristino delle sole testate non annulla
eventuali movimenti successivi; leggere `sql_tests/README.md`.
Configurazione locale `db_connection.json`, log, cache, impostazioni IDE,
registrazioni e vecchi ZIP locali non vengono aggiunti a questo commit.
Conservare separatamente configurazione e backup coerenti dei database
prima di modificare schema o dati per il futuro prelievo abbinato.
## Recuperare esattamente il freeze
Chiudere l'applicazione. Nella cartella del repository, con working tree pulito:
```bat
git fetch origin --tags
git switch --detach warehouse-monolista-2.0
git describe --tags --exact-match
python -m pip install -r requirements.txt
```
Se Git segnala modifiche locali, fermarsi e salvarle prima: non forzare la
sovrascrittura della configurazione o dei sorgenti. Per una prima installazione:
```bat
git clone --branch warehouse-monolista-2.0 --single-branch https://gitea.alessandrobonvicini.it/administrator/ware_house.git C:\flywms-monolista-2.0
```
Il checkout di un tag e' intenzionalmente in detached HEAD: identifica una
versione fissa. `git pull origin main` non e' il comando per restare su questo
freeze. Il ritorno al codice precedente non ripristina il database: eventuali
future patch richiederanno il rispettivo rollback compatibile o un backup.

View File

@@ -29,6 +29,7 @@ SELECT TOP (1)
FROM dbo.py_ViewPackingListPrelievo AS pl
WHERE Ordinamento > 0
AND IDStato = :id_stato
AND (:documento IS NULL OR pl.Documento = TRY_CONVERT(int, :documento))
AND NOT EXISTS (
SELECT 1
FROM dbo.py_BarcodePickingListSkip AS s
@@ -39,7 +40,7 @@ WHERE Ordinamento > 0
AND s.IDStato = pl.IDStato
AND s.Risolto = 0
)
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet;
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet, Documento;
"""
SQL_PICKING_BY_PALLET = """
@@ -55,6 +56,7 @@ SELECT TOP (1)
IDStato
FROM dbo.py_ViewPackingListPrelievo
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
AND (:documento IS NULL OR Documento = TRY_CONVERT(int, :documento))
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet;
"""
@@ -222,6 +224,7 @@ SELECT TOP (1)
COUNT(*) AS SkippedCount
FROM dbo.py_BarcodePickingListSkip AS s
WHERE s.IDStato = :id_stato
AND (:documento IS NULL OR s.Documento = :documento)
AND s.Risolto = 0
AND EXISTS (
SELECT 1
@@ -229,11 +232,20 @@ WHERE s.IDStato = :id_stato
WHERE CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS =
s.Documento COLLATE Latin1_General_CI_AS
AND pl.IDStato = :id_stato
AND CAST(pl.Pallet AS varchar(50)) COLLATE Latin1_General_CI_AS =
s.Pallet COLLATE Latin1_General_CI_AS
)
GROUP BY s.Documento
ORDER BY MIN(s.DataOra);
"""
SQL_PICKING_DOCUMENT_REMAINING = """
SELECT COUNT(DISTINCT Pallet) AS RemainingCount
FROM dbo.py_XMag_ViewPackingListStorico
WHERE Documento = TRY_CONVERT(int, :documento)
AND Cella <> 9999;
"""
SQL_RELEASE_PICKING_DOCUMENT = """
SET NOCOUNT ON;
DECLARE @RC int = 0;
@@ -290,17 +302,21 @@ class BarcodeRepository:
def __init__(self, db_client):
self.db_client = db_client
async def fetch_next_picking(self, id_stato: int) -> dict[str, Any] | None:
"""Return the next pallet proposed by the legacy F1/F2 queue logic."""
async def fetch_next_picking(self, id_stato: int, documento: str | None = None) -> dict[str, Any] | None:
"""Select a document initially, then fetch only within that document."""
res = await self.db_client.query_json(SQL_NEXT_PICKING, {"id_stato": int(id_stato)})
res = await self.db_client.query_json(SQL_NEXT_PICKING, {
"id_stato": int(id_stato), "documento": documento,
})
rows = _rows_to_dicts(res)
return rows[0] if rows else None
async def fetch_picking_by_pallet(self, pallet: str) -> dict[str, Any] | None:
async def fetch_picking_by_pallet(self, pallet: str, documento: str | None = None) -> dict[str, Any] | None:
"""Return one picking row for the given pallet, if still present in the queue."""
res = await self.db_client.query_json(SQL_PICKING_BY_PALLET, {"pallet": str(pallet or "").strip()})
res = await self.db_client.query_json(SQL_PICKING_BY_PALLET, {
"pallet": str(pallet or "").strip(), "documento": documento,
})
rows = _rows_to_dicts(res)
return rows[0] if rows else None
@@ -376,13 +392,21 @@ class BarcodeRepository:
),
)
async def fetch_active_skipped_document(self, id_stato: int) -> dict[str, Any] | None:
"""Return a reserved document whose remaining pallets were all skipped."""
async def fetch_active_skipped_document(self, id_stato: int, documento: str | None = None) -> dict[str, Any] | None:
"""Find skipped residuals, restricted to the active document when set."""
res = await self.db_client.query_json(SQL_ACTIVE_SKIPPED_DOCUMENT, {"id_stato": int(id_stato)})
res = await self.db_client.query_json(SQL_ACTIVE_SKIPPED_DOCUMENT, {
"id_stato": int(id_stato), "documento": documento,
})
rows = _rows_to_dicts(res)
return rows[0] if rows else None
async def count_picking_document_remaining(self, documento: str) -> int:
"""Check residuals independently of priority before declaring completion."""
res = await self.db_client.query_json(SQL_PICKING_DOCUMENT_REMAINING, {"documento": documento})
rows = _rows_to_dicts(res)
return int(rows[0]["RemainingCount"]) if rows else 0
async def release_picking_document(self, *, documento: str, operator_id: int) -> int:
"""Release a reserved picking-list document without closing its residual rows."""

View File

@@ -60,6 +60,8 @@ class BarcodeService:
self.repository = repository
self.operator_id = int(operator_id)
self._current_priority_state = -1
# Independent document identities survive confirmations, F1/F2 and pause.
self._priority_documents: dict[int, str] = {}
self._state = BarcodeViewState()
@property
@@ -72,6 +74,7 @@ class BarcodeService:
"""Return the client to its neutral state."""
self._current_priority_state = -1
self._priority_documents.clear()
self._state = BarcodeViewState()
return self._state
@@ -121,6 +124,7 @@ class BarcodeService:
if state.mode not in ("priority_high", "priority_low"):
return self._state
self._current_priority_state = 1 if state.mode == "priority_high" else 0
self._priority_documents[self._current_priority_state] = state.document
self._state = replace(state, scanned_pallet="", auto_advance_delay_ms=0)
return self._state
@@ -163,20 +167,25 @@ class BarcodeService:
return BarcodeActionResult(True, self._state, self._state.status_text)
async def start_priority_queue(self, id_stato: int) -> BarcodeActionResult:
"""Load the next item of the selected legacy priority queue."""
"""Keep each F1/F2 cycle bound to its document until the cycle stops."""
row = await self.repository.fetch_next_picking(id_stato)
self._current_priority_state = int(id_stato)
id_stato = int(id_stato)
active_document = self._priority_documents.get(id_stato)
row = await self.repository.fetch_next_picking(id_stato, documento=active_document)
self._current_priority_state = id_stato
queue_label = "Alta priorita' (F1)" if int(id_stato) == 1 else "Bassa priorita' (F2)"
if not row:
skipped_doc = await self.repository.fetch_active_skipped_document(int(id_stato))
skipped_doc = await self.repository.fetch_active_skipped_document(id_stato, documento=active_document)
if skipped_doc:
documento = str(skipped_doc.get("Documento") or "").strip()
skipped_count = int(skipped_doc.get("SkippedCount") or 0)
await self.repository.release_picking_document(
rc = await self.repository.release_picking_document(
documento=documento,
operator_id=self.operator_id,
)
if rc != 0:
raise RuntimeError(f"Impossibile sospendere PL {documento}: RC={rc}")
self._priority_documents.pop(id_stato, None)
self._current_priority_state = -1
self._state = BarcodeViewState(
mode="manual_unload",
@@ -187,17 +196,36 @@ class BarcodeService:
destination_readonly=False,
)
return BarcodeActionResult(True, self._state)
status_text = "Pronto."
remaining = 0
if active_document:
remaining = await self.repository.count_picking_document_remaining(active_document)
status_text = (
f"PL {active_document}: {remaining} UDC residue, coda non disponibile. Verifica la prenotazione."
if remaining else f"PL {active_document} terminata. Premi F1 o F2 per iniziare."
)
log_runtime_event("Barcode WMS", f"PICKING CYCLE STOP documento={active_document} priority={id_stato} remaining={remaining}")
# A changed reservation or visibility filter must not silently
# replace an unfinished document on the next press of F1/F2.
if not remaining:
self._priority_documents.pop(id_stato, None)
self._current_priority_state = -1
self._state = BarcodeViewState(
mode="manual_unload",
queue_label=queue_label,
status_text="Pronto.",
status_text=status_text,
status_color=self.RED,
destination_barcode=self.NON_SCAFFALATA_BARCODE,
destination_readonly=False,
)
return BarcodeActionResult(True, self._state)
document = str(row.get("Documento") or "").strip()
if not document or (active_document is not None and document != active_document):
raise RuntimeError(f"Cambio inatteso documento picking: atteso={active_document}, ricevuto={document}")
self._priority_documents[id_stato] = document
if active_document is None:
log_runtime_event("Barcode WMS", f"PICKING CYCLE START documento={document} priority={id_stato}")
customer = f"{row.get('CodNazione') or ''} - {row.get('NAZIONE') or ''}".strip(" -")
source_location = self._display_location(
cella=row.get("Cella"),
@@ -289,7 +317,10 @@ class BarcodeService:
picking_before_move = (
None
if current_location
else await self.repository.fetch_picking_by_pallet(pallet)
else await self.repository.fetch_picking_by_pallet(
pallet,
documento=self._priority_documents.get(self._current_priority_state) if is_picking_unload else None,
)
)
if not current_location and not picking_before_move:
if is_direct_load:
@@ -651,7 +682,9 @@ class BarcodeService:
and last_priority_state in (0, 1)
)
picking_row = (
await self.repository.fetch_picking_by_pallet(barcode_pallet)
await self.repository.fetch_picking_by_pallet(
barcode_pallet, documento=self._priority_documents.get(last_priority_state),
)
if is_priority_shipping_confirmation
else None
)

View File

@@ -0,0 +1,65 @@
# Documento fisso durante il ciclo barcode F1/F2
## Difetto e requisito
Prima della correzione ogni avanzamento interrogava tutte le UDC con IDStato
uguale alla priorita' richiesta. Per F2, che include tutte le liste non
prenotate, il documento poteva quindi cambiare senza intervento dell'operatore.
Una volta proposta una lista, ogni successiva UDC del ciclo deve appartenere
a quel documento. F1 e F2 ricordano ciascuno la propria lista: F2 -> F1 -> F2
deve riportare al documento inizialmente assegnato a F2, anche se cambia
l'ordine globale delle UDC disponibili.
## Implementazione locale
- BarcodeService conserva un documento per priorita', separato dallo stato
visuale temporaneo di conferma o pausa.
- La prima richiesta senza documento usa il criterio di ordinamento vigente.
Non viene introdotta una selezione per data del documento.
- Le successive richieste applicano il filtro Documento nelle query di
avanzamento, ricerca skip e ricerca della UDC per conferma del movimento.
- Un documento differente ricevuto dal repository causa un errore esplicito.
- La pausa per movimenti liberi e il passaggio tra F1/F2 conservano i documenti.
- Quando termina il documento, il ciclo si ferma e mostra il relativo messaggio.
Una successiva pressione esplicita di un tasto priorita' puo' avviare un nuovo
ciclo; la fine del precedente non attiva automaticamente un altro documento.
- Quando rimangono soltanto UDC saltate, viene gestita la sospensione della
lista corrente senza passare a un'altra lista disponibile nella coda.
- Se cambiano prenotazione o visibilita' e ci sono ancora residui nello storico,
compare un messaggio di verifica e il documento viene mantenuto in memoria.
- Un errore della query non cancella il documento memorizzato.
Gli skip sono conteggiati soltanto se la relativa UDC e' ancora residua.
Le movimentazioni libere mantengono la loro logica; il filtro Documento e'
applicato soltanto alle ricerche nel contesto del picking.
## Limiti e installazione
Questa correzione riguarda la sessione del terminalino. La memoria dei documenti
viene azzerata con reset del servizio o riavvio del programma. Non introduce
ancora due prenotazioni persistenti o un percorso automatico comune per colonna:
questi appartengono alla successiva modifica funzionale da specificare.
Non serve una patch SQL: cambiano le SELECT eseguite dal codice Python, non gli
oggetti del database. Versioni: barcode_client 1.0.27 (titolo visibile),
barcode_repository 1.0.14, barcode_service 1.0.25.
## Verifiche
Esecuzione senza GUI e senza movimentazioni reali:
```bat
python -m unittest discover -s tests -p test_barcode_document_binding.py -v
```
Gli 11 test coprono avanzamento con documenti intercalati, passaggi ripetuti
F2/F1/F2, pausa con movimento libero, salto UDC, sospensione con soli skip,
fine lista, cambio prenotazione, errore query, rifiuto di documento inatteso,
isolamento della conferma e passaggio del parametro al repository.
Ulteriori 5 verifiche SELECT su SQL Server, con righe sintetiche in CTE,
hanno controllato selezione iniziale, filtro documento, assenza di fallback,
skip effettivamente residui e isolamento della conferma. Nessun dato reale
e' stato aggiornato per questi test. Il collaudo fisico sul terminalino resta
da effettuare.

View File

@@ -0,0 +1,439 @@
# Prelievo guidato di due picking list sul terminalino
Data: 02/10/2026. Revisione: 0.3.
**Stato: specifica da rileggere e concordare prima dell'implementazione.**
La funzione di prelievo abbinato descritta qui non e' ancora implementata.
I punti indicati come aperti o proposti non sono decisioni gia' approvate.
## 1. Obiettivo operativo
Consentire al responsabile di prenotare una oppure due picking list dal
programma desktop. Quando sono due, il terminalino deve guidare un unico
percorso di prelievo che considera le UDC residue di entrambe.
L'operatore deve percorrere il magazzino seguendo l'ordine previsto, senza
ritornare alle prime colonne perche' una lista non conteneva UDC in quelle
posizioni. Il sistema sceglie di volta in volta il documento della prossima
UDC; l'operatore riconosce il cambio dal numero del documento e dal colore.
Il prelievo abbinato riguarda un operatore che lavora due documenti nello
stesso giro. La gestione di piu' operatori contemporanei richiede ulteriori
decisioni e non e' definita da questa specifica.
## 2. Termini usati
| Termine | Significato |
|---|---|
| Lista 1 / F1 | Prima lista assegnata alla coppia, con identita' stabile |
| Lista 2 / F2 | Seconda lista assegnata alla coppia, con identita' stabile |
| Lista proposta | Documento della prossima UDC indicata dal sistema |
| Colonna o campata | Colonna di uno specifico scaffale e magazzino |
| Cella | Singola posizione dentro la colonna, a un determinato livello |
| Percorso comune | Ordine delle posizioni applicato alle UDC di entrambe le liste |
| UDC residua | UDC ancora da prelevare, non gia' nella locazione spedita |
Area 5 e Area 6 sono i nomi operativi dei magazzini MDE5 e MDE6.
Non corrispondono necessariamente agli ID 5 e 6 della tabella legacy Aree.
La colonna 1 dello scaffale A e la colonna 1 dello scaffale B sono distinte.
## 3. Situazione attuale e difetto da non reintrodurre
La prenotazione Python attuale prevede un unico documento. Storicamente F1
interroga la priorita' prenotata e F2 quella non prenotata. Quest'ultima puo'
contenere piu' documenti, quindi non identifica di per se' una seconda lista.
E' stata introdotta nel codice locale una correzione che fissa il documento
del ciclo per ciascun tasto. Il passaggio F2 -> F1 -> F2 conserva il documento
precedentemente avviato con F2. Il dettaglio e' in
[fix_barcode_documento_fisso_f1_f2.md](fix_barcode_documento_fisso_f1_f2.md).
La nuova funzione estende il modello: entrambi i documenti devono essere
esplicitamente assegnati alla coppia. Il cambio automatico sara' ammesso
soltanto fra questi due documenti e per una ragione di percorso.
Nessuna terza lista deve subentrare silenziosamente.
## 4. Selezione e prenotazione sul desktop
### R01. Una o due liste
Gestione Picking List deve permettere di prenotare al massimo due liste per
il prelievo abbinato. Deve restare utilizzabile anche con una sola lista.
Il semplice clic per consultare il dettaglio non deve modificare implicitamente
una prenotazione: selezione per consultazione e assegnazione al giro devono
avere un comportamento chiaramente distinguibile.
### R02. Identita' della coppia
Il desktop deve rendere riconoscibile quale documento e' Lista 1/F1 e quale
e' Lista 2/F2. L'assegnazione non dipende dall'ordinamento momentaneo della
griglia o dalla posizione della prossima UDC.
Il gesto per assegnare i ruoli e' ancora da definire: ad esempio due slot
espliciti, oppure prima e seconda scelta rese chiaramente visibili.
### R02a. Le due liste devono appartenere alla stessa area
Il prelievo abbinato e' consentito esclusivamente fra due liste della stessa
area operativa: entrambe Area 5/MDE5 oppure entrambe Area 6/MDE6.
Non e' consentito abbinare una lista dell'Area 5 a una dell'Area 6.
Il sistema deve rifiutare l'abbinamento con un messaggio comprensibile,
senza salvare una coppia parziale o alterare le prenotazioni preesistenti.
Il vincolo deve essere verificato anche lato servizio/database, non soltanto
mediante la disponibilita' dei pulsanti nell'interfaccia desktop.
Prima dell'implementazione va definita la fonte autorevole dell'area della
lista, specialmente se contiene UDC non scaffalate o senza giacenza WMS.
La cella virtuale condivisa 1000 non dimostra da sola l'appartenenza all'area.
Non dedurre il magazzino dal numero documento o dagli ID legacy delle Aree.
Il trattamento delle liste internamente miste o con area indeterminata resta
da concordare; non considerarle implicitamente compatibili con entrambe.
### R03. Un'unica sequenza personalizzata
Con due liste selezionate, il comando Sequenza di prelievo riguarda entrambe.
L'editor deve presentare l'unione degli scaffali/gruppi presenti nelle due liste,
includendo quelli presenti soltanto in una di esse e il gruppo Non scaff.
Non si devono eseguire due ordinamenti personalizzati incompatibili nello
stesso giro. La sequenza della coppia deve costituire l'unico riferimento.
La scelta fra eventuali personalizzazioni gia' salvate sulle singole liste
resta aperta. Non e' ammessa una fusione silenziosa che produca un ordine
diverso da quello mostrato al responsabile.
## 5. Percorsi delle aree: conservare il comportamento esistente
### R04. Area 6 / MDE6
Conservare il percorso attuale: scaffali nell'ordine previsto, colonne in
ordine crescente all'interno dello scaffale e livelli dal pavimento verso
l'alto. Il documento [ordinamento.md](ordinamento.md) riporta per i dati
analizzati la sequenza interna a -> b -> c -> d -> e.
La modifica non deve rinumerare Celle.Ordinamento ne' inventare nuovi percorsi
per celle eventualmente configurate con eccezioni.
### R05. Area 5 / MDE5
Conservare l'ordinamento esistente delle celle. Nel default e' il percorso
storico rappresentato da Celle.Ordinamento, con serpentina fra scaffali.
Esempio documentato del blocco 1A/2B:
```text
1A.1 -> 2B.1 -> 2B.2 -> 1A.2 -> 1A.3 -> 2B.3 -> ...
```
All'interno delle campate analizzate l'ordine e' e -> d -> c -> b -> a,
quindi dall'alto verso il basso. E' confermato che anche la prima UDC
proposta deve rispettare questo verso: non partire dal pavimento in area 5.
Non sostituire questo percorso con un generico ordinamento numerico
scaffale/colonna/livello dell'area 6.
### R06. Effetto della personalizzazione
La personalizzazione mantiene il significato attuale: cambia la precedenza
dei gruppi/scaffali, mentre dentro ogni gruppo si conserva l'ordine delle celle.
Con due liste, la stessa scelta si applica all'intero contenuto della coppia.
Per esempio, porre tutto 1A prima di tutto 2B elimina intenzionalmente
l'alternanza fra quei due scaffali, come avviene gia' nella personalizzazione
attuale. Non sarebbe corretto promettere contemporaneamente tale precedenza
e il mantenimento della serpentina globale.
I gruppi omessi da una personalizzazione parziale devono essere accodati con
il criterio attuale, senza perdere le relative UDC.
## 6. Scelta della prossima UDC
### R07. Confronto continuo delle due liste
A ogni passo confrontare le UDC ancora lavorabili di entrambe le liste e
scegliere quella che viene prima nel percorso comune.
Questa regola vale all'avvio, fra colonne consecutive, dopo un movimento e
quando uno dei documenti non ha UDC nella colonna corrente.
Non si deve usare F1 come guida unica, cercando F2 soltanto nelle colonne
incontrate da F1: questo lascerebbe indietro le colonne presenti solo in F2.
### R08. Il percorso prevale sull'identita' della lista
Non esiste una precedenza fissa F1-prima-di-F2 nella stessa colonna.
Se nell'area 6 i livelli dal basso verso l'alto appartengono a F2, F1 e F2,
il prelievo deve seguire F2 -> F1 -> F2.
La precedente ipotesi di esaurire tutte le UDC F1 della colonna e solo dopo
quelle F2 e' superata: la scelta si compie sulla posizione a ogni passo.
### R09. Nessuna UDC persa e nessun documento estraneo
Il contenuto lavorato e' l'unione delle UDC residue delle due liste.
Le UDC su piu' righe lotto devono essere considerate una sola volta come
unita' fisica di prelievo. Ogni proposta deve mantenere l'identita' del
documento a cui appartiene.
Non introdurre la scelta automatica di una terza lista, neanche al termine
di una delle due. La gestione delle UDC saltate e' trattata al punto 10.
## 7. Avvio neutro e tasti F1/F2
### R10. Avvio scelto dal sistema
Quando il terminalino mostra Pronto per la coppia, il sistema deve avere gia'
individuato la prima proposta. L'operatore non decide arbitrariamente da quale
documento partire.
Se la prima UDC appartiene alla Lista 2, F2 e' abilitato e F1 disabilitato;
se appartiene alla Lista 1, vale il contrario. Il blocco deve valere sia per
i pulsanti sullo schermo sia per i tasti fisici.
### R11. Guida durante il giro
Nel prelievo abbinato deve essere selezionabile soltanto la lista proposta
dal percorso. Il cambio automatico verso l'altro documento non richiede una
nuova scelta dell'operatore.
La precedente modalita' manuale F1/F2 e il nuovo giro guidato non devono
essere confusi: nel giro guidato i due tasti identificano documenti stabili,
ma non autorizzano a scavalcare il percorso stabilito.
### Chiarimento confermato: la prima cella dipende dal percorso dell'area
Si parte dalla prima UDC secondo il percorso effettivo comune, rispettando
anche all'avvio l'ordine dell'area e l'eventuale personalizzazione.
La precedente espressione "cella piu' bassa" non e' una regola universale:
- Area 5: nella campata prevista dal percorso, prima la cella occupata
richiesta piu' in alto, poi quelle inferiori (e -> d -> c -> b -> a).
- Area 6: nella colonna prevista dal percorso, prima la cella occupata
richiesta piu' vicina al pavimento, poi quelle superiori (a -> b -> c -> d -> e).
Il confronto comprende entrambe le liste: in area 5, se F1 ha una UDC al
livello a e F2 ne ha una al livello d della stessa campata, si propone prima
F2/d e poi F1/a. Non conta quale documento occupa il ruolo di Lista 1.
Restano la serpentina del default area 5, l'ordine comune eventualmente
personalizzato e la precedenza delle Non scaff. descritta in R14.
## 8. Identificazione visiva sul terminalino
### R12. Colori stabili
| Documento assegnato | Colore di contesto |
|---|---|
| Lista 1 / F1 | Rosellino |
| Lista 2 / F2 | Giallino |
Il colore segue il documento e non la sua posizione nel giro: se inizia F2,
lo sfondo iniziale e' giallino. La stessa lista conserva il proprio colore
nei successivi passaggi.
Il numero della picking list e il ruolo Lista 1/Lista 2 devono essere leggibili
insieme all'UDC e alla cella proposta: il colore non deve essere l'unica
informazione per capire quale documento si sta lavorando.
La barra esito mantiene il significato funzionale dei propri colori
(successo, errore, attesa), distinguibile dallo sfondo di contesto.
Il layout deve restare utilizzabile sul terminalino 240 x 320.
## 9. Ciclo di scansione e conferma
### R13. Sequenza ordinaria
1. Il sistema propone UDC, cella sorgente e documento; applica il colore relativo.
2. L'operatore legge il barcode della UDC proposta.
3. Rimangono i controlli attuali di corrispondenza e avvio automatico della lettura.
4. Il movimento di picking porta la UDC in spedita, 7G.1.1, destinazione 9000000.
5. Il terminalino mostra l'esito positivo per i tre secondi previsti.
6. Il sistema propone la prossima UDC della coppia secondo il percorso comune.
7. Se cambia documento, aggiorna insieme colore, riferimento lista e tasti abilitati.
L'esito dei tre secondi appartiene al movimento appena concluso, non al
documento che verra' proposto dopo. Non deve comparire un contesto misto.
Un cambio programma della UDC visualizzata non deve simulare una scansione
dell'operatore: va preservata la protezione contro gli auto-submit accidentali.
Un errore o un esito incerto non deve avanzare il giro come se il movimento
fosse riuscito; restano necessarie le verifiche e il recupero gia' esistenti.
## 10. Non scaffalate, salti, pausa e fine giro
### R14. Non scaffalate
Conservare il comportamento corrente: Non scaff. precede gli scaffali nel
default, salvo diversa posizione assegnata nella sequenza personalizzata.
La regola deve considerare le UDC di entrambe le liste.
La cella virtuale e' condivisibile e non ha un livello fisico da confrontare.
Il criterio di parita' fra UDC non scaffalate delle due liste resta da
concordare; proposta: ordine stabile per ruolo lista e codice UDC.
### R15. UDC saltate: approfondimento rinviato
Il comando Salta UDC esiste gia'. L'utente ha chiesto di analizzarne in seguito
il comportamento nel nuovo giro abbinato, con l'intenzione di conservare
quanto possibile il funzionamento attuale.
Restano da decidere il termine della singola lista con soli skip, la sua
sprenotazione, la prosecuzione dell'altra lista e le modalita' di ripresa.
Saltare una UDC non deve comunque equivalere a spedirla o cancellarla dalla lista.
### Pausa e ripresa: comportamento da completare
Oggi Pausa PL consente movimenti liberi e ESC riporta al picking.
Nel nuovo giro occorre conservare identita' dei due documenti e percorso.
Va deciso se al rientro mantenere la UDC gia' proposta o ricalcolare la
prossima posizione quando nel frattempo cambiano le ubicazioni.
I movimenti liberi devono mantenere le proprie regole di carico e scarico.
### Fine di una lista: proposta ancora da confermare
Quando una lista non ha piu' UDC residue, proseguire con l'altra mantenendone
ruolo e colore originali. Non promuovere automaticamente F2 a F1 e non
aggiungere un terzo documento. Distinguere esaurimento reale e soli skip.
## 11. Esempi di percorso atteso
### Esempio A: colonne presenti soltanto in F2
Area 6, stesso scaffale, default: colonne crescenti e livelli dal basso
verso l'alto.
| Passo | Posizione | Lista proposta |
|---|---|---|
| 1 | Colonna 1, livello a | F2 |
| 2 | Colonna 2, livello a | F2 |
| 3 | Colonna 3, livello a | F1 |
| 4 | Colonna 3, livello b | F2 |
| 5 | Colonna 4, livello a | F2 |
| 6 | Colonna 5, livello a | F2 |
| 7 | Colonna 6, livello a | F1 |
F1 ha UDC solo ogni tre colonne. Le colonne intermedie di F2 vengono visitate
al momento corretto, senza attendere il termine di F1.
### Esempio B: alternanza verticale nella stessa colonna
| Passo | Posizione | Lista proposta |
|---|---|---|
| 1 | Colonna 3, livello a | F2 |
| 2 | Colonna 3, livello b | F1 |
| 3 | Colonna 3, livello d | F2 |
| 4 | Colonna 4, livello a | F1 |
Il passaggio F2 -> F1 -> F2 e' richiesto dal percorso; i documenti associati
ai tasti restano gli stessi.
### Esempio C: area 5 con default storico
| Ordinamento cella | Ubicazione | Lista proposta |
|---|---|---|
| 1001 | 1A.1.e | F2 |
| 1003 | 1A.1.c | F1 |
| 1007 | 2B.1.e | F1 |
| 1010 | 2B.1.b | F2 |
| 1013 | 2B.2.e | F2 |
| 1019 | 1A.2.e | F1 |
Si uniscono le due liste rispettando i valori del percorso storico; non si
raggruppano automaticamente tutte le colonne 1A prima di quelle 2B.
### Esempio D: ordine personalizzato della coppia
Il responsabile imposta Non scaff. -> 4P -> 1M. Il terminalino lavora tutte
le UDC richieste dalle due liste in Non scaff., poi quelle in 4P, poi quelle
in 1M, applicando dentro ciascun gruppo l'ordine delle celle previsto.
Gli eventuali gruppi non configurati vengono accodati, mai esclusi.
## 12. Garanzie e decisioni tecniche da tradurre in progetto
La prenotazione attuale a documento unico deve evolvere per rappresentare
esplicitamente due documenti con ruoli stabili e una sequenza comune.
Il formato delle nuove tabelle e le modifiche alle stored non sono ancora
definiti da questo documento.
La nuova prenotazione dovra' essere salvata in modo atomico, evitando che il
terminalino legga meta' coppia o una sequenza parzialmente aggiornata.
Le modifiche SQL dovranno avere script di installazione e di ripristino e
mantenere la separazione degli oggetti Python dagli oggetti legacy C#.
Sono ancora da specificare persistenza della ripresa dopo riavvio, modifiche
alla coppia durante un giro, modifiche della sequenza a giro iniziato e
comportamento con piu' terminalini. Non assumere che la memoria locale del
singolo client sia una soluzione completa per questi casi.
## 13. Dati locali predisposti per il collaudo
Il 01/10/2026 sono state riattivate nella copia locale quattro testate SAM
cambiando solo D in P, senza movimentare UDC. Con filtro 30 giorni:
| Lista | UDC residue | In celle fisiche | Non scaffalate nella vista |
|---|---:|---:|---:|
| 233 | 28 | 3 | 25 |
| 234 | 28 | 2 | 26 |
| 240 | 11 | 0 | 11 |
| 241 | 28 | 0 | 28 |
| 244, gia' attiva | 26 | 24 | 2 |
Le celle fisiche candidate sono risultate prive di altre UDC, anche esterne
alle cinque liste. Le UDC residue non sono condivise fra questi documenti.
La condivisione della locazione virtuale Non scaff. e' invece prevista.
Esempi utili in area 5: 234 in 1A.7.a e 244 in 1A.7.d; 233 in 1A.18.a e
244 in 1A.18.c. Sono celle distinte della stessa colonna.
La voce Non scaff. puo' derivare anche dal fallback di una UDC senza giacenza
WMS: non prova da sola l'esistenza di un versamento registrato nella cella 1000.
Questi dati non bastano a verificare tutte le alternanze verticali e le colonne
intermedie in area 6: occorrera' predisporre ulteriori casi controllati prima
del collaudo completo. Non creare documenti ERP fittizi o spostare UDC online
per ottenere tali scenari.
Preparazione e ripristino sono descritti in [sql_tests/README.md](sql_tests/README.md).
Il ripristino delle testate non annulla le movimentazioni fatte durante i test.
## 14. Criteri di accettazione
| Test | Risultato atteso |
|---|---|
| Una sola lista | Ordinamento attuale conservato |
| Due liste entrambe Area 5 oppure entrambe Area 6 | Abbinamento consentito, applicando il percorso della relativa area |
| Una lista Area 5 e una Area 6 | Abbinamento rifiutato senza modificare le prenotazioni esistenti |
| F2 precede F1 nel percorso | All'avvio solo F2 utilizzabile |
| F1 presente ogni tre colonne | Nessuna colonna intermedia F2 omessa |
| Livelli alternati F2/F1/F2 | Tre proposte in ordine fisico, con documenti corretti |
| Default area 5 | Serpentina attuale e livelli dall'alto verso il basso, anche all'avvio |
| Area 5: F1 in 1A.7.a, F2 in 1A.7.d | Prima F2/d, poi F1/a, senza precedenza fissa a F1 |
| Default area 6 | Percorso corrente per scaffale, colonne crescenti e livelli dal basso verso l'alto |
| Personalizzazione della coppia | Stesso ordine applicato a entrambe |
| Gruppo presente solo in F2 | Visibile nell'editor e incluso nel percorso |
| Gruppo omesso dalla personalizzazione | Accodato secondo il comportamento attuale |
| Tasto fisico della lista disabilitata | Nessun cambio documento e nessun movimento |
| Cambio automatico | Numero lista, colore e UDC aggiornati coerentemente |
| UDC con due lotti | Un solo prelievo fisico |
| Terza lista disponibile | Mai inserita automaticamente nel giro |
| Aggiornamento programma del campo UDC | Non interpretato come nuova lettura |
| Movimento fallito o esito incerto | Nessun avanzamento ingiustificato |
I casi relativi a skip, fine parziale, pausa, riavvio e modifiche concorrenti
andranno completati dopo la decisione delle relative regole.
## 15. Decisioni aperte per la revisione
L'avvio e' chiarito: si rispetta il percorso dell'area, dall'alto in area 5
e dal basso in area 6. Restano aperti i punti seguenti.
1. Assegnazione esplicita dei ruoli Lista 1 e Lista 2 sul desktop.
2. Gestione delle sequenze preesistenti quando si compone una nuova coppia.
3. Ordine fra UDC non scaffalate o con identico valore di percorso.
4. Salti, esaurimento con residui e riprenotazione della coppia o di una lista.
5. Ripresa dalla pausa e da un riavvio, con ubicazioni eventualmente cambiate.
6. Modifiche alla coppia/sequenza mentre il prelievo e' in corso.
7. Fonte autorevole dell'area di una lista e gestione di liste internamente
miste o con area indeterminata; le coppie Area 5/Area 6 sono vietate (R02a).
8. Condotta su dati anomali: stessa UDC in due liste o cella fisica con piu' UDC.
Il dataset di collaudo evita questi casi; la regola generale resta da definire.
Le risposte possono essere annotate direttamente in questa sezione prima di
passare al progetto tecnico e alle modifiche applicative.

38
sql_tests/README.md Normal file
View File

@@ -0,0 +1,38 @@
# Dati per il collaudo a due picking list, 01/10/2026
Questi script riguardano esclusivamente la copia di test su HOMESERVER.
Non includerli nella patch cumulativa per la produzione.
`riattiva_pickinglist_20261001.sql` cambia da D a P le testate SAM 233, 234,
240 e 241 dell'anno 2026. Mantiene date, righe, lotti e posizioni delle UDC.
Salva gli stati originali in Mediseawall.dbo.py_TestPickingListReactivation_20261001
e controlla le precondizioni nella stessa transazione dell'aggiornamento.
In Gestione Picking List selezionare **30 giorni** per vedere tutte le liste:
| Lista | UDC residue | In celle fisiche | Non scaffalate nella vista |
|---|---:|---:|---:|
| 233 | 28 | 3 | 25 |
| 234 | 28 | 2 | 26 |
| 240 | 11 | 0 | 11 |
| 241 | 28 | 0 | 28 |
| 244 (gia attiva) | 26 | 24 | 2 |
La 240 conserva altre 33 UDC gia spedite, escluse dalla vista operativa.
La qualifica non scaffalata deriva dalla vista: puo' comprendere UDC che non
hanno una giacenza WMS, mostrate mediante il fallback convenzionale 1000.
Nessuna cella fisica candidata risulta contenere altre UDC. La condivisione
della locazione virtuale non scaffalata e' ammessa. Le liste 233/234 e 244
contengono celle distinte nelle stesse colonne, utili per il futuro test del
cambio automatico fra documenti. Non sono stati creati movimenti di test.
`ripristina_testate_pickinglist_20261001.sql` ripristina solo gli stati SAM.
Non ripristina UDC movimentate, skip o sequenze creati durante le prove.
Per ripetere identicamente un test che modifica le giacenze occorre un backup
coerente dei due database, oppure un ripristino specifico dei dati del test.
La piccola tabella di backup degli stati non sostituisce tale backup completo.
Controllato anche il trigger SAM `StatoLottoRientrato`: il passaggio D -> P
non soddisfa la sua condizione e non modifica i lotti. Il ripristino P -> D
si ferma se esistono lotti marcati T, per evitare gli effetti di quel trigger.

View File

@@ -0,0 +1,82 @@
/* Solo copia locale di test HOMESERVER. Non distribuire come patch produzione.
Riattiva 233,234,240,241 conservando date, righe, UDC e posizioni attuali.
Salva i valori di testata per un rollback mirato; non e' un backup dei DB.
*/
USE Mediseawall;
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF CONVERT(nvarchar(128), SERVERPROPERTY('MachineName')) <> N'HOMESERVER'
THROW 51000, 'Script riservato alla copia di test su HOMESERVER.', 1;
BEGIN TRY
BEGIN TRANSACTION;
IF OBJECT_ID('dbo.py_TestPickingListReactivation_20261001', 'U') IS NOT NULL
THROW 51001, 'Preparazione gia eseguita: controllare il backup prima di ripetere.', 1;
DECLARE @Targets TABLE (ID int PRIMARY KEY, Documento int NOT NULL);
INSERT INTO @Targets VALUES (11882,233),(11883,234),(11890,240),(11891,241);
IF (SELECT COUNT(*) FROM SAMA1.dbo.BAMTES b WITH (UPDLOCK,HOLDLOCK)
JOIN @Targets t ON b.ID=t.ID AND b.NUMDOC=t.Documento
WHERE b.ANNDOC=2026 AND b.STATO='D'
AND b.DATDOC>CAST(GETDATE()-30 AS int)) <> 4
THROW 51002, 'Le quattro testate non corrispondono allo scenario atteso.', 1;
IF EXISTS (
SELECT 1 FROM dbo.py_XMag_ViewPackingListStorico h
JOIN dbo.XMag_GiacenzaPallet g ON g.IDCella=h.Cella
WHERE h.Documento IN (233,234,240,241,244) AND h.Cella NOT IN (1000,9999)
AND LTRIM(RTRIM(g.BarcodePallet)) COLLATE Latin1_General_CI_AS <>
h.Pallet COLLATE Latin1_General_CI_AS
) THROW 51003, 'Cella fisica con altre UDC: preparazione annullata.', 1;
IF EXISTS (
SELECT Pallet FROM dbo.py_XMag_ViewPackingListStorico
WHERE Documento IN (233,234,240,241,244) AND Cella<>9999
GROUP BY Pallet HAVING COUNT(DISTINCT Documento)>1
) THROW 51004, 'Una UDC residua appartiene a piu liste candidate.', 1;
IF EXISTS (SELECT 1 FROM dbo.py_BarcodePickingListSkip
WHERE Documento IN ('233','234','240','241') AND Risolto=0)
OR EXISTS (SELECT 1 FROM dbo.py_PickingListPrelievoSequenza
WHERE Documento IN ('233','234','240','241') AND DelDataOra IS NULL)
OR EXISTS (SELECT 1 FROM dbo.PyPickingListReservation
WHERE NULLIF(LTRIM(RTRIM(Documento)),'') IS NOT NULL)
THROW 51005, 'Prenotazioni, skip o sequenze attive: verificare prima del test.', 1;
CREATE TABLE dbo.py_TestPickingListReactivation_20261001 (
BamtesID int NOT NULL PRIMARY KEY,
Anno smallint NOT NULL,
Documento int NOT NULL,
StatoOriginale char(1) NOT NULL,
DataDocumentoOriginale float NOT NULL,
ApplicataIl datetime2 NOT NULL,
RipristinataIl datetime2 NULL
);
INSERT INTO dbo.py_TestPickingListReactivation_20261001
(BamtesID,Anno,Documento,StatoOriginale,DataDocumentoOriginale,ApplicataIl)
SELECT b.ID,b.ANNDOC,b.NUMDOC,b.STATO,b.DATDOC,SYSDATETIME()
FROM SAMA1.dbo.BAMTES b JOIN @Targets t ON t.ID=b.ID;
UPDATE b SET STATO='P'
FROM SAMA1.dbo.BAMTES b JOIN @Targets t ON t.ID=b.ID;
IF @@ROWCOUNT<>4 THROW 51006, 'Numero di testate aggiornate inatteso.', 1;
IF (SELECT COUNT(DISTINCT Documento) FROM dbo.py_ViewPackingListPrelievo
WHERE Documento IN (233,234,240,241,244)
AND DataDocumento>CAST(GETDATE()-30 AS int))<>5
THROW 51007, 'Le cinque liste non risultano tutte visibili. Rollback.', 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
SELECT Documento,COUNT(DISTINCT Pallet) AS UdcResidue
FROM dbo.py_ViewPackingListPrelievo
WHERE Documento IN (233,234,240,241,244)
GROUP BY Documento ORDER BY Documento;

View File

@@ -0,0 +1,54 @@
/* Ripristina SOLO lo stato delle quattro testate riattivate per i test.
Non annulla movimentazioni effettuate durante il collaudo.
Eseguire sullo stesso DB locale con applicazioni ferme e liste sprenotate.
*/
USE Mediseawall;
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF CONVERT(nvarchar(128),SERVERPROPERTY('MachineName'))<>N'HOMESERVER'
THROW 51000, 'Script riservato alla copia di test su HOMESERVER.', 1;
IF OBJECT_ID('dbo.py_TestPickingListReactivation_20261001','U') IS NULL
THROW 51001, 'Backup delle testate non trovato.', 1;
BEGIN TRY
BEGIN TRANSACTION;
IF EXISTS (
SELECT 1 FROM dbo.PyPickingListReservation r
JOIN dbo.py_TestPickingListReactivation_20261001 s
ON r.Documento=CAST(s.Documento AS varchar(8))
WHERE s.RipristinataIl IS NULL
) THROW 51002, 'Sprenotare le liste di test prima del ripristino.', 1;
IF EXISTS (
SELECT 1 FROM dbo.py_TestPickingListReactivation_20261001 s
LEFT JOIN SAMA1.dbo.BAMTES b WITH (UPDLOCK,HOLDLOCK) ON b.ID=s.BamtesID
WHERE s.RipristinataIl IS NULL AND
(b.ID IS NULL OR b.ANNDOC<>s.Anno OR b.NUMDOC<>s.Documento
OR b.DATDOC<>s.DataDocumentoOriginale OR b.STATO IS NULL
OR b.STATO NOT IN ('P','D'))
) THROW 51003, 'Testate cambiate rispetto al backup: verificare manualmente.', 1;
-- Il trigger SAM StatoLottoRientrato modifica LOTSER.DESCR tornando in D.
-- In presenza di lotti marcati T fermarsi prima di attivarlo.
IF EXISTS (
SELECT 1 FROM dbo.py_TestPickingListReactivation_20261001 s
JOIN SAMA1.dbo.BAMTES b ON b.ID=s.BamtesID
JOIN SAMA1.dbo.FATRIG f ON f.IDBAM=b.ID
JOIN SAMA1.dbo.LOTTIBF lb ON lb.IDFATRIG=f.ID
JOIN SAMA1.dbo.LOTSER l ON l.ID=lb.IDLOTSER
WHERE s.RipristinataIl IS NULL AND b.STATO<>'D' AND l.DESCR='T'
) THROW 51004, 'Lotti rientrati marcati T: ripristino da valutare per il trigger SAM.', 1;
UPDATE b SET STATO=s.StatoOriginale
FROM SAMA1.dbo.BAMTES b
JOIN dbo.py_TestPickingListReactivation_20261001 s ON s.BamtesID=b.ID
WHERE s.RipristinataIl IS NULL;
UPDATE dbo.py_TestPickingListReactivation_20261001
SET RipristinataIl=SYSDATETIME() WHERE RipristinataIl IS NULL;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK TRANSACTION;
THROW;
END CATCH;

View File

@@ -0,0 +1,194 @@
"""Document isolation regressions; no GUI, DB connection or real movements."""
import unittest
from unittest.mock import AsyncMock, patch
from barcode_repository import BarcodeRepository, LegacyMoveResult
from barcode_service import BarcodeService
def row(doc, pallet, priority, order):
return dict(Documento=doc, Pallet=pallet, IDStato=priority,
Ordinamento=order, Cella=1101, Ubicazione="1A - 1 - a")
class PickingRepositoryFake:
def __init__(self):
# Without a document filter the sequence would be 172, 171, 172.
self.rows = [row("173", "600001", 1, 5), row("172", "600002", 0, 10),
row("171", "600003", 0, 20), row("172", "600004", 0, 30)]
self.skipped = set()
self.locations = {r["Pallet"]: r["Cella"] for r in self.rows}
self.released = []
async def fetch_next_picking(self, id_stato, documento=None):
candidates = [r for r in self.rows if r["IDStato"] == id_stato
and (documento is None or r["Documento"] == documento)
and r["Pallet"] not in self.skipped]
return min(candidates, key=lambda r: r["Ordinamento"]) if candidates else None
async def fetch_active_skipped_document(self, id_stato, documento=None):
candidates = [r for r in self.rows if r["IDStato"] == id_stato
and (documento is None or r["Documento"] == documento)
and r["Pallet"] in self.skipped]
return dict(Documento=candidates[0]["Documento"], SkippedCount=len(candidates)) if candidates else None
async def count_picking_document_remaining(self, documento):
return len({r["Pallet"] for r in self.rows if r["Documento"] == documento})
async def release_picking_document(self, *, documento, operator_id):
self.released.append(documento)
return 0
async def skip_picking_pallet(self, *, documento, pallet, **kwargs):
self.skipped.add(pallet)
async def fetch_picking_by_pallet(self, pallet, documento=None):
return next((r for r in self.rows if r["Pallet"] == pallet
and (documento is None or r["Documento"] == documento)), None)
async def fetch_trace_by_pallet(self, pallet):
return dict(Lotto="LOT", Prodotto="ART", Descrizione="Test")
async def fetch_current_location_by_pallet(self, pallet):
return dict(IDCella=self.locations[pallet])
async def fetch_open_locations_by_pallet(self, pallet):
return [await self.fetch_current_location_by_pallet(pallet)]
async def fetch_closed_picking_by_pallet(self, pallet):
return None
async def execute_legacy_move(self, *, barcode_pallet, barcode_cella, **kwargs):
target = 9999 if barcode_cella == "9000000" else 1000
self.locations[barcode_pallet] = target
if target == 9999:
self.rows = [r for r in self.rows if r["Pallet"] != barcode_pallet]
return LegacyMoveResult(0, barcode_cella, barcode_pallet, int(barcode_cella))
class DocumentBindingTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
logger = patch("barcode_service.log_runtime_event")
logger.start()
self.addCleanup(logger.stop)
self.repo = PickingRepositoryFake()
self.service = BarcodeService(self.repo, operator_id=1)
async def ship_current(self):
result = await self.service.submit(
scanned_pallet=self.service.state.expected_pallet, destination_barcode="9000000")
self.assertTrue(result.ok)
self.assertEqual(result.state.auto_advance_delay_ms, 3000)
async def test_auto_advance_never_interleaves_low_priority_documents(self):
await self.service.start_priority_queue(0)
await self.ship_current()
result = await self.service.start_priority_queue(0)
self.assertEqual((result.state.document, result.state.expected_pallet), ("172", "600004"))
async def test_f2_f1_f2_stays_bound_even_if_global_order_changes(self):
await self.service.start_priority_queue(0)
self.repo.rows[2]["Ordinamento"] = 1
for _ in range(5):
high = await self.service.start_priority_queue(1)
self.assertEqual(high.state.document, "173")
low = await self.service.start_priority_queue(0)
self.assertEqual((low.state.document, low.state.expected_pallet), ("172", "600002"))
async def test_pause_free_move_resume_and_switch_preserve_document(self):
await self.service.start_priority_queue(0)
paused = self.service.state
self.service.begin_priority_pause(0)
free = await self.service.submit(scanned_pallet="600003", destination_barcode="9001000")
self.assertTrue(free.ok)
self.service.resume_priority_state(paused)
await self.service.start_priority_queue(1)
low = await self.service.start_priority_queue(0)
self.assertEqual(low.state.document, "172")
self.assertEqual(low.state.expected_pallet, "600002")
async def test_skip_advances_only_within_active_document(self):
await self.service.start_priority_queue(0)
await self.service.skip_current_picking_pallet()
low = await self.service.start_priority_queue(0)
self.assertEqual(low.state.expected_pallet, "600004")
async def test_only_skipped_residuals_stop_even_with_other_lists_available(self):
await self.service.start_priority_queue(0)
await self.service.skip_current_picking_pallet()
await self.service.start_priority_queue(0)
await self.service.skip_current_picking_pallet()
result = await self.service.start_priority_queue(0)
self.assertEqual(result.state.mode, "manual_unload")
self.assertIn("PL 172 sospesa", result.state.status_text)
self.assertEqual(self.repo.released, ["172"])
async def test_completion_stops_before_next_document(self):
await self.service.start_priority_queue(0)
await self.ship_current()
await self.service.start_priority_queue(0)
await self.ship_current()
stopped = await self.service.start_priority_queue(0)
self.assertEqual(stopped.state.mode, "manual_unload")
self.assertIn("PL 172 terminata", stopped.state.status_text)
self.assertEqual(stopped.state.auto_advance_delay_ms, 0)
# A later explicit F2 can start another list, after the stop message.
next_list = await self.service.start_priority_queue(0)
self.assertEqual(next_list.state.document, "171")
async def test_priority_change_blocks_without_forgetting_previous_document(self):
await self.service.start_priority_queue(0)
for r in self.repo.rows:
if r["Documento"] == "172":
r["IDStato"] = 1
for _ in range(2):
stopped = await self.service.start_priority_queue(0)
self.assertIn("PL 172", stopped.state.status_text)
self.assertIn("Verifica la prenotazione", stopped.state.status_text)
for r in self.repo.rows:
if r["Documento"] == "172":
r["IDStato"] = 0
resumed = await self.service.start_priority_queue(0)
self.assertEqual(resumed.state.document, "172")
async def test_query_exception_keeps_document_for_retry(self):
await self.service.start_priority_queue(0)
with patch.object(self.repo, "fetch_next_picking", side_effect=TimeoutError):
with self.assertRaises(TimeoutError):
await self.service.start_priority_queue(0)
self.repo.rows[2]["Ordinamento"] = 1
result = await self.service.start_priority_queue(0)
self.assertEqual(result.state.document, "172")
async def test_wrong_document_from_repository_is_rejected(self):
await self.service.start_priority_queue(0)
with patch.object(self.repo, "fetch_next_picking", return_value=self.repo.rows[2]):
with self.assertRaisesRegex(RuntimeError, "Cambio inatteso documento"):
await self.service.start_priority_queue(0)
result = await self.service.start_priority_queue(0)
self.assertEqual(result.state.document, "172")
async def test_confirmation_cannot_display_another_document_for_same_pallet(self):
await self.service.start_priority_queue(0)
self.repo.rows.append(row("171", "600002", 0, 1))
self.repo.fetch_picking_by_pallet = AsyncMock(wraps=self.repo.fetch_picking_by_pallet)
state = await self.service._build_post_move_state(
barcode_pallet="600002", destination_barcode="9000000",
destination_display="7G.1.1", last_priority_state=0, auto_advance_delay_ms=3000)
self.assertEqual(state.document, "172")
self.repo.fetch_picking_by_pallet.assert_awaited_once_with("600002", documento="172")
async def test_repository_passes_document_to_all_related_queries(self):
db = type("DB", (), {"query_json": AsyncMock(return_value={"rows": []})})()
repo = BarcodeRepository(db)
await repo.fetch_next_picking(0, documento="172")
await repo.fetch_active_skipped_document(0, documento="172")
await repo.fetch_picking_by_pallet("600002", documento="172")
for call in db.query_json.await_args_list:
self.assertEqual(call.args[1]["documento"], "172")
self.assertIn(":documento", call.args[0])
if __name__ == "__main__":
unittest.main()

View File

@@ -13,9 +13,9 @@ MODULE_VERSIONS: dict[str, str] = {
"async_msssql_query": "1.0.1",
"audit_log": "1.0.0",
"main": "1.0.2",
"barcode_client": "1.0.26",
"barcode_repository": "1.0.13",
"barcode_service": "1.0.24",
"barcode_client": "1.0.27",
"barcode_repository": "1.0.14",
"barcode_service": "1.0.25",
"busy_overlay": "1.0.0",
"db_config": "1.0.0",
"diagnostica": "1.0.4",