# 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`.
