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
|
FROM dbo.py_ViewPackingListPrelievo AS pl
|
||||||
WHERE Ordinamento > 0
|
WHERE Ordinamento > 0
|
||||||
AND IDStato = :id_stato
|
AND IDStato = :id_stato
|
||||||
|
AND (:documento IS NULL OR pl.Documento = TRY_CONVERT(int, :documento))
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM dbo.py_BarcodePickingListSkip AS s
|
FROM dbo.py_BarcodePickingListSkip AS s
|
||||||
@@ -39,7 +40,7 @@ WHERE Ordinamento > 0
|
|||||||
AND s.IDStato = pl.IDStato
|
AND s.IDStato = pl.IDStato
|
||||||
AND s.Risolto = 0
|
AND s.Risolto = 0
|
||||||
)
|
)
|
||||||
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet;
|
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet, Documento;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SQL_PICKING_BY_PALLET = """
|
SQL_PICKING_BY_PALLET = """
|
||||||
@@ -55,6 +56,7 @@ SELECT TOP (1)
|
|||||||
IDStato
|
IDStato
|
||||||
FROM dbo.py_ViewPackingListPrelievo
|
FROM dbo.py_ViewPackingListPrelievo
|
||||||
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
|
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;
|
ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -222,6 +224,7 @@ SELECT TOP (1)
|
|||||||
COUNT(*) AS SkippedCount
|
COUNT(*) AS SkippedCount
|
||||||
FROM dbo.py_BarcodePickingListSkip AS s
|
FROM dbo.py_BarcodePickingListSkip AS s
|
||||||
WHERE s.IDStato = :id_stato
|
WHERE s.IDStato = :id_stato
|
||||||
|
AND (:documento IS NULL OR s.Documento = :documento)
|
||||||
AND s.Risolto = 0
|
AND s.Risolto = 0
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -229,11 +232,20 @@ WHERE s.IDStato = :id_stato
|
|||||||
WHERE CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS =
|
WHERE CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS =
|
||||||
s.Documento COLLATE Latin1_General_CI_AS
|
s.Documento COLLATE Latin1_General_CI_AS
|
||||||
AND pl.IDStato = :id_stato
|
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
|
GROUP BY s.Documento
|
||||||
ORDER BY MIN(s.DataOra);
|
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 = """
|
SQL_RELEASE_PICKING_DOCUMENT = """
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
DECLARE @RC int = 0;
|
DECLARE @RC int = 0;
|
||||||
@@ -290,17 +302,21 @@ class BarcodeRepository:
|
|||||||
def __init__(self, db_client):
|
def __init__(self, db_client):
|
||||||
self.db_client = db_client
|
self.db_client = db_client
|
||||||
|
|
||||||
async def fetch_next_picking(self, id_stato: int) -> dict[str, Any] | None:
|
async def fetch_next_picking(self, id_stato: int, documento: str | None = None) -> dict[str, Any] | None:
|
||||||
"""Return the next pallet proposed by the legacy F1/F2 queue logic."""
|
"""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)
|
rows = _rows_to_dicts(res)
|
||||||
return rows[0] if rows else None
|
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."""
|
"""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)
|
rows = _rows_to_dicts(res)
|
||||||
return rows[0] if rows else None
|
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:
|
async def fetch_active_skipped_document(self, id_stato: int, documento: str | None = None) -> dict[str, Any] | None:
|
||||||
"""Return a reserved document whose remaining pallets were all skipped."""
|
"""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)
|
rows = _rows_to_dicts(res)
|
||||||
return rows[0] if rows else None
|
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:
|
async def release_picking_document(self, *, documento: str, operator_id: int) -> int:
|
||||||
"""Release a reserved picking-list document without closing its residual rows."""
|
"""Release a reserved picking-list document without closing its residual rows."""
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ class BarcodeService:
|
|||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.operator_id = int(operator_id)
|
self.operator_id = int(operator_id)
|
||||||
self._current_priority_state = -1
|
self._current_priority_state = -1
|
||||||
|
# Independent document identities survive confirmations, F1/F2 and pause.
|
||||||
|
self._priority_documents: dict[int, str] = {}
|
||||||
self._state = BarcodeViewState()
|
self._state = BarcodeViewState()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -72,6 +74,7 @@ class BarcodeService:
|
|||||||
"""Return the client to its neutral state."""
|
"""Return the client to its neutral state."""
|
||||||
|
|
||||||
self._current_priority_state = -1
|
self._current_priority_state = -1
|
||||||
|
self._priority_documents.clear()
|
||||||
self._state = BarcodeViewState()
|
self._state = BarcodeViewState()
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ class BarcodeService:
|
|||||||
if state.mode not in ("priority_high", "priority_low"):
|
if state.mode not in ("priority_high", "priority_low"):
|
||||||
return self._state
|
return self._state
|
||||||
self._current_priority_state = 1 if state.mode == "priority_high" else 0
|
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)
|
self._state = replace(state, scanned_pallet="", auto_advance_delay_ms=0)
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@@ -163,20 +167,25 @@ class BarcodeService:
|
|||||||
return BarcodeActionResult(True, self._state, self._state.status_text)
|
return BarcodeActionResult(True, self._state, self._state.status_text)
|
||||||
|
|
||||||
async def start_priority_queue(self, id_stato: int) -> BarcodeActionResult:
|
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)
|
id_stato = int(id_stato)
|
||||||
self._current_priority_state = 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)"
|
queue_label = "Alta priorita' (F1)" if int(id_stato) == 1 else "Bassa priorita' (F2)"
|
||||||
if not row:
|
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:
|
if skipped_doc:
|
||||||
documento = str(skipped_doc.get("Documento") or "").strip()
|
documento = str(skipped_doc.get("Documento") or "").strip()
|
||||||
skipped_count = int(skipped_doc.get("SkippedCount") or 0)
|
skipped_count = int(skipped_doc.get("SkippedCount") or 0)
|
||||||
await self.repository.release_picking_document(
|
rc = await self.repository.release_picking_document(
|
||||||
documento=documento,
|
documento=documento,
|
||||||
operator_id=self.operator_id,
|
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._current_priority_state = -1
|
||||||
self._state = BarcodeViewState(
|
self._state = BarcodeViewState(
|
||||||
mode="manual_unload",
|
mode="manual_unload",
|
||||||
@@ -187,17 +196,36 @@ class BarcodeService:
|
|||||||
destination_readonly=False,
|
destination_readonly=False,
|
||||||
)
|
)
|
||||||
return BarcodeActionResult(True, self._state)
|
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._current_priority_state = -1
|
||||||
self._state = BarcodeViewState(
|
self._state = BarcodeViewState(
|
||||||
mode="manual_unload",
|
mode="manual_unload",
|
||||||
queue_label=queue_label,
|
queue_label=queue_label,
|
||||||
status_text="Pronto.",
|
status_text=status_text,
|
||||||
status_color=self.RED,
|
status_color=self.RED,
|
||||||
destination_barcode=self.NON_SCAFFALATA_BARCODE,
|
destination_barcode=self.NON_SCAFFALATA_BARCODE,
|
||||||
destination_readonly=False,
|
destination_readonly=False,
|
||||||
)
|
)
|
||||||
return BarcodeActionResult(True, self._state)
|
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(" -")
|
customer = f"{row.get('CodNazione') or ''} - {row.get('NAZIONE') or ''}".strip(" -")
|
||||||
source_location = self._display_location(
|
source_location = self._display_location(
|
||||||
cella=row.get("Cella"),
|
cella=row.get("Cella"),
|
||||||
@@ -289,7 +317,10 @@ class BarcodeService:
|
|||||||
picking_before_move = (
|
picking_before_move = (
|
||||||
None
|
None
|
||||||
if current_location
|
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 not current_location and not picking_before_move:
|
||||||
if is_direct_load:
|
if is_direct_load:
|
||||||
@@ -651,7 +682,9 @@ class BarcodeService:
|
|||||||
and last_priority_state in (0, 1)
|
and last_priority_state in (0, 1)
|
||||||
)
|
)
|
||||||
picking_row = (
|
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
|
if is_priority_shipping_confirmation
|
||||||
else None
|
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",
|
"async_msssql_query": "1.0.1",
|
||||||
"audit_log": "1.0.0",
|
"audit_log": "1.0.0",
|
||||||
"main": "1.0.2",
|
"main": "1.0.2",
|
||||||
"barcode_client": "1.0.26",
|
"barcode_client": "1.0.27",
|
||||||
"barcode_repository": "1.0.13",
|
"barcode_repository": "1.0.14",
|
||||||
"barcode_service": "1.0.24",
|
"barcode_service": "1.0.25",
|
||||||
"busy_overlay": "1.0.0",
|
"busy_overlay": "1.0.0",
|
||||||
"db_config": "1.0.0",
|
"db_config": "1.0.0",
|
||||||
"diagnostica": "1.0.4",
|
"diagnostica": "1.0.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user