Alpha6 barcode non scaffalate e bypass login

This commit is contained in:
2026-06-18 16:13:47 +02:00
parent cc9680c49a
commit 466778ae5f
19 changed files with 1614 additions and 48 deletions

44
docs/review/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Documentazione review Warehouse / FlyWMS bridge
Questa cartella contiene la documentazione tecnica destinata alla review del codice Python e SQL.
## Obiettivi
- Descrivere il processo logico implementato da ogni modulo.
- Documentare firme, parametri, ritorni e responsabilita' di funzioni e classi.
- Documentare SQL, viste, stored procedure, tabelle coinvolte ed effetti collaterali.
- Aggiungere commenti inline nel codice solo dove il comportamento non e' auto-evidente.
## Criterio di documentazione
La documentazione e' divisa in tre livelli.
1. Processo logico: flussi operativi, eventi UI, query, effetti sul database.
2. API interna: classi, funzioni, parametri, valori di ritorno, errori, dipendenze.
3. Dettaglio implementativo: strutture dati, cicli, dedupliche, transazioni, side effect.
## Ambito escluso
Questa review non considera:
- `__pycache__`
- log applicativi
- zip di distribuzione
- cartelle `_package_*`
- cartella `trash`
- build HTML generata in `docs/_build`
## Primo lotto documentato
- [Modulo storico picking list](module_storico_pickinglist.md)
- [SQL storico picking list](sql_storico_pickinglist.md)
- [Modulo gestione picking list](module_gestione_pickinglist.md)
- [Modulo prenota/sprenota SQL](module_prenota_sprenota_sql.md)
- [SQL prenotazione picking list](sql_pickinglist_reservation.md)
## Prossimi lotti consigliati
1. `gestione_scarico.py`
2. `barcode_service.py` e `barcode_repository.py`
3. `main.py`, `login_window.py`, `db_config.py`
4. Moduli di visualizzazione: `gestione_layout.py`, `reset_corsie.py`, `search_pallets.py`, `view_celle_multi_udc.py`

View File

@@ -0,0 +1,68 @@
# Barcode: logging errori e transazioni
## Problema rilevato
Durante uno scarico picking list dal lettore barcode, l'operazione DB e' arrivata a buon fine ma il client ha mostrato un'eccezione. I file standard output/error erano vuoti.
Con `pythonw` non e' sufficiente affidarsi a stdout/stderr: eccezioni Tk, future asincrone o callback possono non arrivare al wrapper principale.
## Strategia adottata
Il client barcode ora registra errori in modo esplicito su `warehouse_fatal.log` e su `barcode_wms_launch.log`.
Canali coperti:
- `sys.excepthook`
- `threading.excepthook`
- `Tk.report_callback_exception`
- exception handler del loop `asyncio`
- eccezioni delle operazioni asincrone barcode
- errori del repository durante movimento DB
- errori post-movimento durante ricostruzione stato UI
## Transazione movimento DB
Il movimento barcode passa da `BarcodeRepository.execute_legacy_move`.
La batch SQL usa:
```sql
SET XACT_ABORT ON;
```
ed e' eseguita con:
```python
query_json(..., commit=True)
```
Questo significa:
- successo: commit;
- eccezione SQL/Python: rollback automatico della transazione SQLAlchemy;
- il valore `@RC` restituito dalla stored legacy non viene interpretato come errore.
Nota importante: in `sp_xMagGestioneMagazziniPallet`, quando il movimento va a buon fine, `@RC` viene valorizzato con `@@IDENTITY`, cioe' con l'identificativo del movimento inserito. Quindi un valore diverso da zero e' compatibile con un movimento riuscito e non deve causare rollback.
## Distinzione tra movimento e refresh UI
Dopo un movimento riuscito, il service ricostruisce il messaggio UI interrogando picking list e tracciabilita'.
Se questa fase post-movimento fallisce:
- il movimento resta valido;
- l'errore viene loggato;
- l'utente vede `Movimento eseguito. Dettagli non aggiornati.`;
- non viene mostrato il messaggio "transazione non completata".
Se invece fallisce la transazione vera:
- l'utente vede `Transazione non completata, ripeti l'operazione.`;
- il dettaglio tecnico viene scritto nel log.
## File principali
- `runtime_support.py`
- `barcode_client.py`
- `barcode_repository.py`
- `barcode_service.py`

View File

@@ -0,0 +1,119 @@
# Piano operativo documentazione review
## Strategia
La documentazione viene prodotta per lotti piccoli e verificabili. Ogni lotto deve contenere:
- descrizione del processo logico;
- tabella firme funzioni/classi;
- documentazione SQL collegata;
- eventuali commenti inline mirati;
- compilazione Python dei moduli modificati.
## Lotto 1 - Storico picking list
Stato: completato.
File documentati:
- `storico_pickinglist.py`
- `apply_python_pickinglist_history_views.sql`
- `apply_online_history_forms_patch.sql`
- query runtime `SQL_STORICO_PL`
- query runtime `SQL_STORICO_PL_DETAILS`
- movimento collegato `move_pallet_async`
Motivo priorita:
- contiene una nuova funzione con effetti sul database;
- gestisce casi critici `Chiusa ERP con residui`;
- usa convenzioni operative `1000 / Non scaff.` e `9999 / 7G.1.1`.
## Lotto 2 - Gestione picking list
Stato: completato.
File target:
- `gestione_pickinglist.py`
- `prenota_sprenota_sql.py`
- `apply_python_parallel_pickinglist_patch.sql`
- `rollback_python_parallel_pickinglist_patch.sql`
Aspetti da documentare:
- prenotazione/sprenotazione Python-only;
- differenza tra vista residua e vista storica;
- aggiornamento griglia alta/bassa;
- stato `IDStato`;
- separazione da C# legacy;
- limiti concorrenza.
## Lotto 3 - Movimento UDC e scarico
File target:
- `gestione_scarico.py`
- `storico_udc.py`
Aspetti da documentare:
- batch `SQL_SCARICA_UDC`;
- movimento `P` e `V`;
- audit utenti/date;
- cella source e target;
- fallback SPED in storico UDC.
## Lotto 4 - Barcode
File target:
- `barcode_client.py`
- `barcode_service.py`
- `barcode_repository.py`
Aspetti da documentare:
- stati operativi del barcode;
- F1/F2 e priorita picking list;
- gestione `9000000`;
- mappatura 1:1 con comportamento C#;
- stabilita di connessione DB.
## Lotto 5 - Avvio, autenticazione e configurazione
File target:
- `main.py`
- `login_window.py`
- `db_config.py`
- `runtime_support.py`
- `user_session.py`
Aspetti da documentare:
- single instance;
- login operatore;
- creazione configurazione DB;
- gestione errori in `pythonw`;
- shutdown ordinato.
## Lotto 6 - Visualizzazioni operative
File target:
- `gestione_layout.py`
- `reset_corsie.py`
- `search_pallets.py`
- `view_celle_multi_udc.py`
- `ui_tables.py`
- `ui_theme.py`
Aspetti da documentare:
- griglie e layout;
- query diagnostiche;
- overlay async;
- colori semantici;
- export XLSX;
- bonifica UDC fantasma.

View File

@@ -0,0 +1,154 @@
# Modulo `gestione_pickinglist.py`
## Scopo
Il modulo implementa la finestra operativa "Gestione Picking List". La finestra serve al backoffice/magazzino per:
- visualizzare le picking list ancora lavorabili;
- selezionare una sola lista alla volta;
- visualizzare le UDC residue della lista selezionata;
- prenotare una lista per darle priorita' alta sul barcode;
- s-prenotare la lista correntemente prenotata;
- mantenere UI reattiva tramite query asincrone, overlay e spinner.
La finestra lavora sulla vista residuale `dbo.py_ViewPackingListRestante`, quindi mostra cio' che resta da scaricare, non necessariamente il documento ERP completo. Il documento completo e' invece esposto dalla form "Storico Picking List".
## Processo logico
### Apertura finestra
1. `open_pickinglist_window` crea o riporta in primo piano una singola finestra.
2. La finestra viene inizialmente nascosta con `withdraw` e alpha `0.0`.
3. Viene creato `GestionePickingListFrame`.
4. Dopo la stabilizzazione layout, `_first_show` avvia il primo caricamento.
5. La finestra viene mostrata solo quando il layout e' pronto, riducendo flicker.
### Caricamento lista alta
1. `reload_from_db` esegue `SQL_PL`.
2. `SQL_PL` legge da `dbo.py_ViewPackingListRestante`.
3. Il risultato viene normalizzato con `_rows_to_dicts`.
4. `_refresh_mid_rows` ricrea la tabella alta.
5. Ogni riga e' rappresentata da un `PLRow`, che incapsula dizionario dati e checkbox.
### Selezione lista
1. Il click sul checkbox chiama `on_row_checked`.
2. La selezione e' esclusiva: eventuali altre righe selezionate vengono deselezionate.
3. Il documento selezionato viene salvato in `detail_doc`.
4. Viene eseguita `SQL_PL_DETAILS`.
5. Il dettaglio viene messo in cache in `_detail_cache`.
6. `_refresh_details_incremental` ordina e renderizza il dettaglio con `tksheet`.
### Ricarica
`reload_from_db` preserva il documento selezionato quando possibile. Questo e' importante per il caso operativo in cui il barcode sta scaricando UDC mentre l'operatore aggiorna la finestra.
Flusso:
1. `_selected_documento_for_reload` determina il documento da mantenere.
2. La griglia alta viene ricaricata.
3. `_reselect_documento_after_reload` riseleziona la stessa lista se ancora presente.
4. Il dettaglio viene ricaricato dalla vista residuale.
### Prenotazione
1. `on_prenota` verifica il permesso `pickinglist.prenota`.
2. Richiede una riga selezionata.
3. Se `IDStato == 1`, l'operazione e' no-op.
4. Verifica sessione operatore valida.
5. Chiama `sp_xExePackingListPallet_async(..., Azione="P")`.
6. Se `rc == 0`, logga audit e ricarica la griglia mantenendo il documento.
### S-prenotazione
1. `on_sprenota` verifica il permesso `pickinglist.sprenota`.
2. Richiede una riga selezionata.
3. Se `IDStato == 0`, l'operazione e' no-op.
4. Chiama `sp_xExePackingListPallet_async(..., Azione="S")`.
5. Se `rc == 0`, logga audit e ricarica.
## Semantica operativa
La UI non implementa toggle implicito. I pulsanti hanno semantica esplicita:
| Pulsante | Effetto |
| --- | --- |
| `Prenota` | porta la lista selezionata a priorita' alta se non gia' prenotata |
| `S-prenota` | libera la lista selezionata se e' la lista attiva |
Premere piu' volte `Prenota` sulla stessa lista gia' prenotata non la libera. Premere piu' volte `S-prenota` su lista non prenotata non la prenota.
## Strutture dati principali
| Nome | Tipo | Ruolo |
| --- | --- | --- |
| `rows_models` | `list[PLRow]` | modelli riga della griglia alta |
| `_detail_cache` | `dict[Any, list]` | cache righe dettaglio per documento |
| `detail_doc` | `Any` | documento correntemente selezionato |
| `_detail_sort_key` | `str | None` | colonna dettaglio ordinata |
| `_detail_sort_reverse` | `bool` | verso ordinamento dettaglio |
| `pl_table` | `ScrollTable` | tabella custom griglia alta |
| `detail_sheet` | `tksheet.Sheet` | griglia dettaglio ad alto volume |
| `spinner` | `ToolbarSpinner` | feedback leggero toolbar |
| `busy` | `InlineBusyOverlay` | overlay query/operazioni lente |
## Funzioni e classi principali
| Oggetto | Firma | Ruolo |
| --- | --- | --- |
| `_rows_to_dicts` | `_rows_to_dicts(res: Dict[str, Any]) -> List[Dict[str, Any]]` | normalizza payload DB |
| `_s` | `_s(v) -> str` | converte `None` in stringa vuota |
| `_first` | `_first(d: Dict[str, Any], keys: List[str], default: str = "")` | estrae il primo campo valorizzato |
| `ColSpec` | `dataclass(title, key, width, anchor)` | descrive una colonna tabellare |
| `ToolbarSpinner` | classe | animazione leggera toolbar |
| `ScrollTable` | classe | tabella custom per griglia alta |
| `PLRow` | classe | modello riga PL + checkbox |
| `GestionePickingListFrame` | classe | frame principale |
| `_build_detail_sheet` | `_build_detail_sheet(self)` | crea tksheet dettaglio |
| `_detail_rows_to_sheet_data` | `_detail_rows_to_sheet_data(self, rows)` | converte righe DB in righe tksheet |
| `_load_detail_sheet_data` | `_load_detail_sheet_data(self, data)` | applica headers, dati e zebra |
| `_refresh_mid_rows` | `_refresh_mid_rows(self, rows)` | ricrea griglia alta |
| `_get_selected_model` | `_get_selected_model(self) -> Optional[PLRow]` | ritorna riga selezionata |
| `on_row_checked` | `on_row_checked(self, model: PLRow, is_checked: bool)` | carica dettaglio della lista |
| `reload_from_db` | `reload_from_db(self, first=False, reselect_documento=None)` | aggiorna griglia alta |
| `_refresh_details` | `_refresh_details(self)` | ridisegna dettaglio da cache |
| `_refresh_details_incremental` | `_refresh_details_incremental(self, batch_size=25)` | render dettaglio con overlay |
| `on_prenota` | `on_prenota(self)` | prenota lista selezionata |
| `on_sprenota` | `on_sprenota(self)` | s-prenota lista selezionata |
| `create_frame` | `create_frame(parent, *, db_client=None, conn_str=None, session=None)` | factory frame |
| `open_pickinglist_window` | `open_pickinglist_window(parent, db_client, session=None) -> tk.Misc` | entry point finestra |
## Query runtime
### `SQL_PL`
Aggrega `dbo.py_ViewPackingListRestante` per documento. Conta UDC con:
```sql
COUNT(DISTINCT NULLIF(LTRIM(RTRIM(CAST(Pallet AS varchar(32)))), '')) AS Pallet
```
Questo evita il doppio conteggio di UDC multi-lotto.
### `SQL_PL_DETAILS`
Ritorna tutte le righe residuali della lista selezionata, ordinate per `Ordinamento`.
## Effetti sul database
Il modulo UI non aggiorna direttamente tabelle. Tutte le modifiche passano da `sp_xExePackingListPallet_async`, che esegue `dbo.py_sp_xExePackingListPallet`.
Effetti indiretti:
- aggiorna `dbo.PyPickingListReservation`;
- aggiorna `dbo.Celle.IDStato`;
- scrive audit applicativo con `log_user_action`;
- la stored puo' scrivere log legacy tramite `sp_LogPackingList`.
## Rischi e note review
- La vista residuale cambia mentre il barcode scarica UDC: per questo `Ricarica` conserva la selezione e ricarica il dettaglio.
- La tabella alta e il dettaglio possono non avere lo stesso numero di righe dello storico, perche' qui si vedono solo residui.
- La convivenza C#/Python dipende dalla presenza degli oggetti `py_*`.
- La selezione e' esclusiva lato UI, ma il secondo livello F2 barcode dipende dalla logica barcode/stato DB, non da una seconda prenotazione backoffice.

View File

@@ -0,0 +1,97 @@
# Modulo `prenota_sprenota_sql.py`
## Scopo
Il modulo contiene il port asincrono della stored procedure di prenotazione picking list. E' il ponte tra la UI Python e la stored SQL Python-only `dbo.py_sp_xExePackingListPallet`.
La funzione pubblica principale e':
```python
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult
```
## Motivazione
Durante la fase di test sul campo devono convivere:
- applicazione C# legacy;
- applicazione Python.
Per evitare collisioni, il Python non chiama `dbo.sp_xExePackingListPallet` legacy ma `dbo.py_sp_xExePackingListPallet`.
## Semantica parametro `Azione`
| Azione | Significato |
| --- | --- |
| `P` | prenota il documento |
| `S` | s-prenota il documento |
Ogni altra azione ritorna:
```python
SPResult(rc=-10, message="Azione non valida: ...")
```
## Classe `SPResult`
Firma:
```python
@dataclass
class SPResult:
rc: int = 0
message: Optional[str] = ""
id_result: Optional[int] = None
```
Campi:
| Campo | Ruolo |
| --- | --- |
| `rc` | codice ritorno della stored |
| `message` | messaggio errore o vuoto |
| `id_result` | predisposizione per ID risultato, non usato oggi |
## Funzioni helper
| Funzione | Firma | Ruolo |
| --- | --- | --- |
| `_query_one_value` | `_query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any]` | helper generico scalar, oggi non centrale nel flusso |
| `_query_all` | `_query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]` | normalizza query multi-riga |
| `_execute` | `_execute(db, sql: str, params: Dict[str, Any]) -> int` | helper generico DML |
| `sp_xExePackingListPallet_async` | vedi sopra | esegue stored Python-only |
## Processo logico `sp_xExePackingListPallet_async`
1. Normalizza `Azione` a maiuscolo.
2. Valida `Azione in ("P", "S")`.
3. Costruisce una batch SQL con output parameter `@RC`.
4. Esegue:
```sql
EXEC dbo.py_sp_xExePackingListPallet
@IDOperatore = :IDOperatore,
@Documento = :Documento,
@Azione = :Azione,
@RC = @RC OUTPUT;
```
5. Legge il valore `RC`.
6. Ritorna `SPResult(rc=rc, message="", id_result=None)`.
7. In caso di eccezione ritorna `SPResult(rc=-1, message=str(exc), id_result=None)`.
## Commit
La chiamata usa:
```python
commit=True
```
Motivo: la stored modifica `PyPickingListReservation` e `Celle.IDStato`.
## Rischi e note review
- Gli helper `_query_one_value`, `_query_all`, `_execute` sono generici e non tutti sono essenziali al percorso attuale.
- La funzione cattura le eccezioni e ritorna `SPResult(rc=-1)`, quindi la UI deve controllare sempre `rc`.
- La transazione reale dipende dal comportamento di `query_json(..., commit=True)` nel client DB.

View File

@@ -0,0 +1,150 @@
# Modulo `storico_pickinglist.py`
## Scopo
Il modulo implementa la finestra "Storico Picking List". La finestra permette di:
- cercare picking list storiche per numero documento;
- visualizzare lo stato operativo sintetico di ciascuna lista;
- visualizzare il dettaglio UDC/lotto/articolo della lista selezionata;
- evidenziare liste chiuse, liste attive e casi anomali;
- versare in blocco in `7G.1.1` le UDC residue di una lista classificata come `Chiusa ERP con residui`.
Il modulo legge da viste SQL dedicate al ramo Python. Non usa le viste legacy C# per evitare collisioni durante il periodo in cui i due programmi convivono sullo stesso database.
## Dipendenze principali
- `customtkinter` e `tkinter.ttk`: UI e griglie.
- `AsyncRunner`: esecuzione asincrona delle query senza bloccare la GUI.
- `InlineBusyOverlay`: indicatore visivo durante query e movimenti.
- `move_pallet_async`: funzione centrale per registrare un movimento UDC verso una cella.
- `ui_tables`: stile comune delle griglie.
- `version_info`: versione visibile nel titolo finestra.
- `window_placement`: posizionamento della finestra rispetto al launcher.
## Processo logico
### Apertura finestra
1. `open_storico_pickinglist_window(parent, db_client, session)` crea `StoricoPickingListWindow`.
2. La finestra viene posizionata sotto il launcher con `place_window_fullsize_below_parent_later`.
3. Dopo 300 ms viene chiamato `_load_master`.
### Caricamento griglia alta
1. `_load_master` legge il filtro `Documento`.
2. Viene eseguita `SQL_STORICO_PL`.
3. `_rows_to_dicts` normalizza il payload del DB.
4. `_fill_master` svuota griglia alta e dettaglio.
5. Per ogni documento viene calcolato il tag visuale:
- `warning` se lista aperta con UDC gia' spedite;
- `done` se lista chiusa o esaurita;
- `active` se lista prenotata;
- zebra tag per alternanza righe.
6. La riga viene inserita nella griglia alta.
### Selezione documento
1. `_on_master_select` legge la riga selezionata.
2. Memorizza:
- `_selected_documento`
- `_selected_stato_operativo`
- `_detail_rows`
3. Abilita o disabilita il pulsante "Versa residui in 7G.1.1".
4. Esegue `SQL_STORICO_PL_DETAILS`.
5. `_fill_detail` carica la griglia bassa.
### Versamento residui in 7G.1.1
Il pulsante e' abilitato solo se `_selected_stato_operativo == "Chiusa ERP con residui"`.
Flusso:
1. `_ship_selected_residuals` verifica lo stato selezionato.
2. Calcola una stima delle UDC residue gia' presenti nel dettaglio UI.
3. Mostra una conferma all'operatore.
4. Dentro il job asincrono rilegge il dettaglio dal DB, cosi' evita dati UI obsoleti.
5. `_residual_pallets_from_rows` deduplica le UDC e scarta quelle gia' in `IDCella = 9999`.
6. Per ogni UDC residua chiama `move_pallet_async` con:
- `target_idcella = 9999`
- `target_barcode_cella = "9000000"`
- `utente = login sessione` oppure `warehouse_ui`
7. Al termine mostra un riepilogo.
8. Ricarica la griglia alta e tenta di ripristinare la selezione del documento.
## Stati operativi
La colonna `Stato` della griglia alta deriva da SQL:
| Stato operativo | Condizione |
| --- | --- |
| `Chiusa ERP con residui` | `StatoDocumento = 'D'` e almeno una UDC non e' in `9999` |
| `Chiusa` | `StatoDocumento = 'D'` e nessuna UDC residua |
| `Esaurita` | tutte le UDC risultano spedite |
| `In corso` | almeno una UDC e' spedita e almeno una resta da lavorare |
| `Da lavorare` | nessuna UDC ancora spedita |
## Strutture dati interne
| Nome | Tipo | Ruolo |
| --- | --- | --- |
| `var_documento` | `tk.StringVar` | filtro documento digitato dall'utente |
| `_selected_documento` | `str | None` | documento correntemente selezionato |
| `_selected_stato_operativo` | `str` | stato operativo della riga selezionata |
| `_detail_rows` | `list[dict[str, Any]]` | ultimo dettaglio normalizzato caricato nella griglia bassa |
| `master_tree` | `ttk.Treeview` | griglia alta documenti |
| `detail_tree` | `ttk.Treeview` | griglia bassa righe UDC |
| `btn_ship_residuals` | `ctk.CTkButton` | azione di versamento massivo residui |
## Funzioni e classi
| Oggetto | Firma | Ruolo |
| --- | --- | --- |
| `_rows_to_dicts` | `_rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]` | normalizza payload DB in lista di dizionari |
| `_format_date` | `_format_date(value: Any) -> str` | converte date SQL/SAMA in `dd/mm/yyyy` |
| `StoricoPickingListWindow.__init__` | `__init__(self, parent: tk.Widget, db_client, session=None)` | costruisce stato, tema, runner async e UI |
| `_build_ui` | `_build_ui(self) -> None` | crea toolbar, griglia alta e griglia dettaglio |
| `_make_tree` | `_make_tree(self, *, row: int, columns: tuple[str, ...], widths: dict[str, int]) -> ttk.Treeview` | factory griglia con stile comune |
| `_load_master` | `_load_master(self) -> None` | lancia query master asincrona |
| `_fill_master` | `_fill_master(self, rows: list[dict[str, Any]]) -> None` | popola griglia alta |
| `_on_master_select` | `_on_master_select(self, _event=None) -> None` | gestisce selezione documento e carica dettaglio |
| `_fill_detail` | `_fill_detail(self, rows: list[dict[str, Any]]) -> None` | popola griglia bassa |
| `_update_residual_button` | `_update_residual_button(self) -> None` | abilita azione solo su `Chiusa ERP con residui` |
| `_restore_master_selection` | `_restore_master_selection(self, documento: str) -> None` | riseleziona documento dopo reload |
| `_residual_pallets_from_rows` | `_residual_pallets_from_rows(self, rows: list[dict[str, Any]]) -> list[str]` | deduplica UDC residue non gia' spedite |
| `_operator_login` | `_operator_login(self) -> str` | determina utente per movimenti |
| `_ship_selected_residuals` | `_ship_selected_residuals(self) -> None` | esegue versamento massivo verso `7G.1.1` |
| `open_storico_pickinglist_window` | `open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc` | entry point chiamato dal launcher |
## Effetti sul database
Il modulo e' in sola lettura tranne `_ship_selected_residuals`.
`_ship_selected_residuals` non esegue SQL diretto di update. Delega ogni UDC a `move_pallet_async`, che:
- trova l'ultimo movimento `V` positivo della UDC;
- aggiorna `ModUtente` e `ModDataOra` della riga sorgente;
- inserisce un movimento `P` sulla cella sorgente;
- libera `Celle.IDStato` della cella sorgente;
- inserisce un movimento `V` sulla cella target `9999`;
- esegue `dbo.py_sp_ControllaPrenotazionePackingListPalletNew`.
## Rischi e note review
- Il versamento massivo non e' atomico a livello di intera lista: ogni UDC viene movimentata con una chiamata separata. Se una UDC fallisce, quelle precedenti potrebbero essere gia' state registrate.
- La rilettura del dettaglio riduce il rischio di dato UI obsoleto, ma non sostituisce un lock transazionale su documento.
- Le UDC multi-lotto sono deduplicate per `Pallet`, per evitare doppio movimento fisico.
- Le UDC gia' in `9999` vengono saltate.
- Le UDC non scaffalate risultano `IDCella = 1000`; vengono considerate residue e quindi movimentate verso `9999`.
## Test consigliati
1. Selezionare una lista `Da lavorare`: il pulsante deve restare disabilitato.
2. Selezionare una lista `Chiusa`: il pulsante deve restare disabilitato.
3. Selezionare una lista `Chiusa ERP con residui`: il pulsante deve abilitarsi.
4. Confermare il versamento su una lista con UDC non scaffalate.
5. Verificare in `Storico movimenti UDC` che ogni UDC abbia:
- ultimo passaggio sorgente;
- movimento `P` sulla sorgente;
- movimento `V` su `9999 / 7G.1.1`.
6. Ricaricare lo storico picking list: la lista non deve piu' risultare con residui se tutte le UDC sono in `9999`.

View File

@@ -0,0 +1,153 @@
# SQL prenotazione picking list Python-only
## Script
- `apply_python_parallel_pickinglist_patch.sql`
- `rollback_python_parallel_pickinglist_patch.sql`
## Obiettivo
Permettere a Python e C# di lavorare sullo stesso database senza condividere le stesse stored procedure operative.
Il C# continua a usare gli oggetti legacy:
- `dbo.XMag_ViewPackingList`
- `dbo.ViewPackingListRestante`
- `dbo.sp_xExePackingListPallet`
- `dbo.sp_xExePackingListPalletPrenota`
- `dbo.sp_ControllaPrenotazionePackingListPalletNew`
Il Python usa oggetti separati:
- `dbo.py_XMag_ViewPackingList`
- `dbo.py_ViewPackingListRestante`
- `dbo.py_sp_xExePackingListPallet`
- `dbo.py_sp_xExePackingListPalletPrenota`
- `dbo.py_sp_ControllaPrenotazionePackingListPalletNew`
- `dbo.PyPickingListReservation`
## Tabella `dbo.PyPickingListReservation`
### Scopo
Contiene la singola picking list prenotata dal programma Python.
### Schema
| Colonna | Tipo | Ruolo |
| --- | --- | --- |
| `ID` | `tinyint` | sempre `1`, enforced da check constraint |
| `Documento` | `varchar(8)` | documento prenotato |
| `IDOperatore` | `int` | operatore che ha fatto l'ultima azione |
| `ModUtente` | `varchar(50)` | login operatore |
| `ModDataOra` | `datetime` | timestamp modifica |
### Vincoli
- Primary key su `ID`.
- Check `ID = 1`.
Questa scelta implementa un singleton DB: il Python puo' avere una sola lista prenotata dal backoffice.
## Vista `dbo.py_XMag_ViewPackingList`
### Scopo
Replica la vista legacy `dbo.XMag_ViewPackingList` ma calcola `IDStato` usando `PyPickingListReservation`, non `Celle.IDStato` legacy.
### Logica `IDStato`
```sql
CASE
WHEN pr.Documento IS NOT NULL
AND pr.Documento = CAST(legacy.Documento AS varchar(8))
THEN 1
ELSE 0
END AS IDStato
```
## Vista `dbo.py_ViewPackingListRestante`
### Scopo
Espone solo le UDC non ancora spedite:
```sql
WHERE Cella <> 9999
```
La UI "Gestione Picking List" usa questa vista sia per la griglia alta sia per il dettaglio.
## Stored `dbo.py_sp_xExePackingListPallet`
### Parametri
| Parametro | Tipo | Ruolo |
| --- | --- | --- |
| `@IDOperatore` | `int` | operatore corrente |
| `@Documento` | `varchar(8)` | documento da prenotare/sprenotare |
| `@Azione` | `char(1)` | `P` prenota, `S` s-prenota |
| `@RC` | `int OUTPUT` | codice ritorno |
### Validazioni
- `@Azione` deve essere `P` o `S`, altrimenti `@RC = -10`.
- Il documento deve esistere in `py_XMag_ViewPackingList`, altrimenti `@RC = -20`.
- Se manca la riga singleton in `PyPickingListReservation`, viene creata.
### Prenotazione `P`
1. Se il documento e' gia' attivo, ritorna senza modifiche.
2. Azzera `IDStato` su tutte le celle con stato diverso da zero.
3. Mette `IDStato = 1` sulle celle del documento target.
4. Aggiorna `PyPickingListReservation` con documento, operatore e timestamp.
5. Scrive log tramite `sp_LogPackingList`.
### S-prenotazione `S`
1. Se il documento attivo non e' quello richiesto, ritorna senza modifiche.
2. Azzera `IDStato` sulle celle target.
3. Pulisce `PyPickingListReservation.Documento`.
## Stored `dbo.py_sp_xExePackingListPalletPrenota`
### Scopo
Mette `IDStato = 1` sulle celle ancora residue del documento. Viene usata dal controllo automatico prenotazione.
## Stored `dbo.py_sp_ControllaPrenotazionePackingListPalletNew`
### Scopo
Mantiene coerente la prenotazione mentre la picking list viene consumata dal barcode.
### Logica
1. Legge documento attivo da `PyPickingListReservation`.
2. Se non c'e' documento attivo, termina.
3. Se non esistono piu' righe residue in `py_ViewPackingListRestante`:
- azzera `Celle.IDStato`;
- svuota `PyPickingListReservation.Documento`;
- termina.
4. Se esistono ancora residui:
- azzera tutti gli `IDStato`;
- richiama `py_sp_xExePackingListPalletPrenota` per prenotare le celle residue.
## Rollback
`rollback_python_parallel_pickinglist_patch.sql` elimina:
- `py_sp_ControllaPrenotazionePackingListPalletNew`
- `py_sp_xExePackingListPalletPrenota`
- `py_sp_xExePackingListPallet`
- `py_ViewPackingListRestante`
- `py_XMag_ViewPackingList`
- `PyPickingListReservation`
Non tocca gli oggetti legacy C#.
## Note review
- La stored Python usa una prenotazione singleton, quindi non gestisce due liste prenotate backoffice.
- L'azzeramento globale di `Celle.IDStato` e' coerente con la semantica attuale, ma in futuro dovra' essere rivalutato se piu' operatori o magazzini lavorano in parallelo.
- Le UDC non scaffalate condividono il fallback dati del modello legacy; questo e' noto e andra' riprogettato nel nuovo schema FlyWMS.

View File

@@ -0,0 +1,236 @@
# SQL storico picking list
## Oggetti SQL coinvolti
La form "Storico Picking List" usa un ramo SQL Python-only:
- `dbo.py_vPreparaPackingListSAMA1`
- `dbo.py_vPreparaPackingList`
- `dbo.py_XMag_ViewPackingListStorico`
- `dbo.PyPickingListReservation`
Gli script principali sono:
- `apply_python_pickinglist_history_views.sql`
- `apply_online_history_forms_patch.sql`
- `rollback_python_pickinglist_history_views.sql`
- `rollback_online_history_forms_patch.sql`
## Motivazione del ramo Python-only
Il progetto deve convivere temporaneamente con l'applicazione C# legacy. Per questo lo storico Python non modifica:
- `dbo.vPreparaPackingListSAMA1`
- `dbo.vPreparaPackingList`
- `dbo.XMag_ViewPackingList`
- stored procedure legacy C#
Le viste prefissate `py_` permettono di evolvere il comportamento Python senza cambiare la semantica legacy.
## Vista `dbo.py_vPreparaPackingListSAMA1`
### Scopo
Legge dal database ERP `SAMA1` le informazioni documentali e di riga necessarie a ricostruire la picking list.
### Tabelle ERP coinvolte
- `SAMA1.dbo.LOTSER`
- `SAMA1.dbo.ARTICO`
- `SAMA1.dbo.FATRIG`
- `SAMA1.dbo.LOTTIBF`
- `SAMA1.dbo.BAMTES`
- `SAMA1.dbo.NAZIONI`
- `SAMA1.sam.EXTUC`
- `SAMA1.dbo.MTRASP`
### Campi principali
| Campo | Significato |
| --- | --- |
| `NUMLOT` | lotto |
| `CODICE` | codice articolo |
| `DESCR` | descrizione riga/articolo |
| `UDC` | prime 6 cifre di `LOTSER.NUMSER` |
| `Qta` | somma quantita giacente ERP |
| `NUMDOC` | numero documento/picking list |
| `DATDOC` | data documento ERP |
| `StatoDocumento` | stato ERP, filtrato su `P` o `D` |
| `NAZIONE` | trasporto + descrizione nazione |
### Filtri
- anno documento >= anno corrente;
- stato documento in `P`, `D`;
- lotto diverso da `00000000000`.
## Vista `dbo.py_vPreparaPackingList`
### Scopo
E' una vista di pulizia sopra `py_vPreparaPackingListSAMA1`.
### Filtro
Esclude righe con `NUMLOT = ''`.
## Vista `dbo.py_XMag_ViewPackingListStorico`
### Scopo
Unisce il contenuto ERP della picking list con la posizione WMS corrente della UDC.
### Tabelle WMS coinvolte
- `dbo.XMag_GiacenzaPallet`
- `dbo.Celle`
- `dbo.Aree`
- `dbo.PyPickingListReservation`
### Join principale
La vista usa una `RIGHT OUTER JOIN` tra posizione WMS e preparazione ERP:
```sql
RIGHT OUTER JOIN dbo.py_vPreparaPackingList prep
ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC
```
Questo garantisce che una UDC presente nella picking list ERP compaia anche se non e' scaffalata nel WMS.
### Convenzioni celle
| IDCella | Ubicazione | Significato |
| --- | --- | --- |
| `9999` | `7G - 1 - 1` | spedizione / UDC scaricata |
| `1000` | `Non scaff.` | fallback per UDC non trovata in `XMag_GiacenzaPallet` |
La vista usa:
```sql
ISNULL(g.IDCella, 1000) AS Cella
ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione
```
Quindi le UDC ERP non presenti in giacenza WMS sono rappresentate come non scaffalate.
## Query Python `SQL_STORICO_PL`
### Scopo
Costruisce la griglia alta della form.
### CTE
| CTE | Ruolo |
| --- | --- |
| `base` | normalizza `Pallet` in `PalletKey` |
| `pallets` | aggrega righe multi-lotto per singola UDC |
| `meta` | calcola metadati documento |
| `agg` | calcola totale UDC, UDC spedite e UDC residue |
### Stato operativo
La query produce `StatoOperativo` con questa logica:
```sql
CASE
WHEN m.StatoDocumento = 'D' AND COALESCE(a.RigheResidue, 0) > 0 THEN 'Chiusa ERP con residui'
WHEN m.StatoDocumento = 'D' THEN 'Chiusa'
WHEN COALESCE(a.RigheResidue, 0) = 0 THEN 'Esaurita'
WHEN COALESCE(a.RigheSpedite, 0) > 0 THEN 'In corso'
ELSE 'Da lavorare'
END
```
### Deduplica UDC
La deduplica avviene su `PalletKey`, non sulle righe. Questo evita che una UDC con piu' lotti venga contata come piu' pallet.
## Query Python `SQL_STORICO_PL_DETAILS`
### Scopo
Costruisce la griglia bassa della form.
### Output
| Campo | Ruolo |
| --- | --- |
| `Documento` | numero picking list |
| `Pallet` | UDC fisica |
| `Lotto` | lotto |
| `Articolo` | codice articolo |
| `Descrizione` | descrizione articolo/riga |
| `Qta` | quantita |
| `DataDocumento` | data ERP |
| `StatoDocumento` | stato ERP |
| `Cella` | cella WMS corrente/fallback |
| `Ubicazione` | descrizione cella |
| `Ordinamento` | ordine operativo |
| `IDStato` | prenotazione Python |
## Effetti SQL del versamento residui
La form non contiene direttamente update/insert SQL per il versamento residui. Chiama `move_pallet_async` in `gestione_scarico.py`.
La batch SQL associata e' `SQL_SCARICA_UDC` e produce:
1. ricerca dell'ultimo movimento `V` positivo della UDC;
2. update audit della riga sorgente;
3. insert movimento `P` sulla cella sorgente;
4. update `Celle.IDStato = 0` sulla cella sorgente;
5. insert movimento `V` sulla cella target;
6. esecuzione `dbo.py_sp_ControllaPrenotazionePackingListPalletNew`.
Per la funzione storico picking list il target e':
- `target_idcella = 9999`
- `target_barcode_cella = '9000000'`
## Note di consistenza dati
- Una UDC non scaffalata viene vista come `Cella = 1000`, quindi e' residua.
- Una UDC in `9999` non e' residua e viene esclusa dal movimento massivo.
- Una UDC multi-lotto puo' comparire piu' volte nel dettaglio ma deve generare un solo movimento fisico.
- Lo stato `Chiusa ERP con residui` evidenzia disallineamento tra documento ERP chiuso e WMS non completamente versato in spedizione.
## Query diagnostiche utili
Verificare righe documento:
```sql
SELECT *
FROM dbo.py_XMag_ViewPackingListStorico
WHERE Documento = 155
ORDER BY Ordinamento, Pallet;
```
Verificare UDC residue:
```sql
SELECT DISTINCT Pallet, Cella, Ubicazione
FROM dbo.py_XMag_ViewPackingListStorico
WHERE Documento = 155
AND ISNULL(Cella, 0) <> 9999
ORDER BY Pallet;
```
Verificare movimenti UDC dopo versamento:
```sql
SELECT
ID,
Tipo,
IDRiferimento,
Attributo,
IDCella,
DataMagazzino,
InsUtente,
InsDataOra,
ModUtente,
ModDataOra
FROM dbo.MagazziniPallet
WHERE Attributo = '655560'
ORDER BY ID;
```