Freeze 2.0 warehouse monolista e documento fisso F1/F2
This commit is contained in:
70
FREEZE_2.0_WAREHOUSE_MONOLISTA.md
Normal file
70
FREEZE_2.0_WAREHOUSE_MONOLISTA.md
Normal 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.
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
65
fix_barcode_documento_fisso_f1_f2.md
Normal file
65
fix_barcode_documento_fisso_f1_f2.md
Normal 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.
|
||||
439
specifica_prelievo_due_pickinglist.md
Normal file
439
specifica_prelievo_due_pickinglist.md
Normal 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
38
sql_tests/README.md
Normal 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.
|
||||
82
sql_tests/riattiva_pickinglist_20261001.sql
Normal file
82
sql_tests/riattiva_pickinglist_20261001.sql
Normal 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;
|
||||
54
sql_tests/ripristina_testate_pickinglist_20261001.sql
Normal file
54
sql_tests/ripristina_testate_pickinglist_20261001.sql
Normal 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;
|
||||
194
tests/test_barcode_document_binding.py
Normal file
194
tests/test_barcode_document_binding.py
Normal 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()
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user