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