# Analisi bug prenotazione Picking List

## Obiettivo

Documentare in modo chiaro:

- il comportamento attuale della prenotazione picking list
- il bug osservato
- la causa tecnica del bug
- la correzione proposta lato stored procedure

Il documento serve come base di riferimento prima di modificare la logica SQL legacy.

---

## Contesto operativo

L'operatore, dal backoffice:

1. seleziona una picking list
2. preme `Prenota`
3. si aspetta che **una sola** picking list risulti prenotata

Il comportamento atteso del sistema è:

- una sola picking list prenotata alla volta
- quella picking list diventa la coda `F1` sul barcode
- le altre picking list restano non prenotate e alimentano la coda `F2`

---

## Comportamento attuale

Il comportamento effettivamente osservato è questo:

1. l'operatore seleziona una sola picking list
2. preme `Prenota`
3. risultano prenotate **due** picking list, non una sola

Questo comportamento è stato osservato:

- nell'applicazione Python
- anche nell'applicazione C#

Quindi il bug **non nasce nella UI Python**.

---

## Verifica di aderenza Python / C#

### Lato Python

La finestra [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py) chiama:

- [sp_xExePackingListPallet_async](C:/devel/python/ware_house/prenota_sprenota_sql.py)

che esegue direttamente la stored procedure SQL legacy:

- `dbo.sp_xExePackingListPallet`

### Lato C#

La finestra legacy [FRMagViewLayout.cs](C:/devel/seawall_decompiled/decompiled/SWMagViewLayout/FRMagViewLayout.cs) chiama anch'essa:

- `dbo.sp_xExePackingListPallet`

### Conclusione

Python e C# usano la **stessa** logica DB per la prenotazione.

Se il bug compare in entrambi, il problema è nella stored procedure o nel modello dati su cui essa si basa.

---

## Stato attuale della griglia picking list

La query summary della picking list in [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py) aggrega così:

- `GROUP BY Documento, CodNazione, NAZIONE, Stato`
- `MAX(IDStato) AS IDStato`

Questo significa:

- se anche **una sola riga** del documento ha `IDStato = 1`
- l'intera picking list appare come prenotata

Questa scelta è coerente con l'idea UI di “documento prenotato”, ma diventa problematica se la stored non lavora in modo esclusivo per documento.

---

## Stored procedure attuale

Stored coinvolta:

- [sp_xExePackingListPallet](C:/devel/python/ware_house/script.sql)

### Logica attuale

La stored:

1. legge da `XMag_ViewPackingList`
2. estrae le `Cella` associate al `Documento`
3. per ogni cella:
   - se `IDStato = 0`, la mette a `1`
   - altrimenti la rimette a `0`

Quindi la stored non prenota davvero una picking list come entità esclusiva.

Prenota invece:

- **le celle** toccate dal documento selezionato

### Conseguenza tecnica

Se due documenti condividono almeno una stessa cella:

- la prenotazione della prima list mette `IDStato = 1` sulla cella condivisa
- anche la seconda list, leggendo quella stessa cella, eredita `IDStato = 1`
- la griglia alta, usando `MAX(IDStato)`, la mostra come prenotata

---

## Evidenze raccolte

### Query 1 - celle del sottoinsieme di documenti osservati

Scopo:

- verificare quali celle sono coinvolte dai documenti sospetti
- controllare rapidamente se esistono sovrapposizioni evidenti

Query:

```sql
SELECT
    Documento,
    Cella,
    COUNT(*) AS Righe
FROM dbo.XMag_ViewPackingList
WHERE Documento IN (<doc1>, <doc2>)
GROUP BY Documento, Cella
ORDER BY Cella, Documento;
```

Uso nel caso analizzato:

- ha permesso di vedere, per esempio, che documenti diversi insistono sulla stessa cella `8057`

### Query di controllo celle condivise

Risultato:

```text
1000    2
8057    2
```

Interpretazione:

- la cella `1000` è condivisa da 2 documenti
- la cella `8057` è condivisa da 2 documenti

### Query di dettaglio documenti / celle condivise

Risultato:

```text
135    1000    16    16
137    1000    2     1
133    8057    1     1
135    8057    1     1
```

Interpretazione:

- documento `135` e documento `137` condividono la cella `1000`
- documento `133` e documento `135` condividono la cella `8057`

Questa è una prova concreta del fatto che la prenotazione a livello cella può propagarsi a più documenti.

### Query 2 - ricerca globale delle celle condivise

Scopo:

- trovare quali celle sono condivise da più documenti
- identificare i punti strutturalmente più critici del dataset

Query:

```sql
SELECT
    Cella,
    COUNT(DISTINCT Documento) AS NumDocumenti
FROM dbo.XMag_ViewPackingList
GROUP BY Cella
HAVING COUNT(DISTINCT Documento) > 1
ORDER BY NumDocumenti DESC, Cella;
```

Risultato osservato:

```text
1000    2
8057    2
```

Interpretazione:

- la cella `1000` è condivisa da 2 documenti
- la cella `8057` è condivisa da 2 documenti

### Query 3 - dettaglio completo delle collisioni sulle celle condivise

Scopo:

- capire esattamente quali documenti condividono le celle critiche
- vedere quante righe e quanti pallet distinti insistono su quelle celle

Query:

```sql
SELECT
    Documento,
    Cella,
    COUNT(*) AS Righe,
    COUNT(DISTINCT Pallet) AS Pallet
FROM dbo.XMag_ViewPackingList
WHERE Cella IN (1000, 8057)
GROUP BY Documento, Cella
ORDER BY Cella, Documento;
```

Risultato osservato:

```text
135    1000    16    16
137    1000    2     1
133    8057    1     1
135    8057    1     1
```

Interpretazione:

- documento `135` e documento `137` condividono la cella `1000`
- documento `133` e documento `135` condividono la cella `8057`

Queste tre query, lette insieme, sono quelle che hanno permesso di evidenziare in modo oggettivo il bug.

---

## Ruolo delle celle convenzionali

Dalle verifiche fatte con il magazziniere:

- `1000` = `5E1.1`
- `9999` = `7G.1.1`

La cella `1000` è una locazione convenzionale delle UDC non scaffalate.

Questo rende il problema ancora più frequente, perché:

- più documenti possono convivere sulla stessa locazione convenzionale `1000`
- quindi la prenotazione per cella tende naturalmente a contaminare più picking list

---

## Diagnosi finale del bug

Il bug è dovuto alla combinazione di due scelte:

1. la stored `sp_xExePackingListPallet` lavora a livello **cella**
2. la UI mostra la prenotazione a livello **documento**

Quando esistono celle condivise tra documenti:

- la prenotazione di un documento produce `IDStato = 1` anche su righe lette da altri documenti
- quindi più picking list risultano prenotate

### Forma sintetica del bug

> Il sistema non sta prenotando una singola picking list in modo esclusivo.  
> Sta prenotando le celle associate a quel documento, e la UI deduce da lì lo stato della picking list.

---

# Correzione proposta

## Obiettivo della correzione

Allineare il comportamento del sistema alla regola di business desiderata:

> una sola picking list prenotata per volta

Questo implica che il comando `Prenota` deve produrre uno stato esclusivo a livello documento.

---

## Principio della nuova logica

La stored non deve più comportarsi come un semplice toggle delle celle del documento selezionato.

Deve invece:

1. rimuovere la prenotazione da tutte le altre picking list
2. applicare la prenotazione solo al documento selezionato

In altre parole:

- **prima** si resetta lo stato prenotato degli altri documenti
- **poi** si prenota il documento scelto

---

## Comportamento desiderato dopo la correzione

### Caso `Prenota`

Quando l'operatore prenota il documento `D`:

1. se il documento `D` è già prenotato, non succede nulla
2. se il documento `D` non è prenotato:
   - tutte le celle prenotate appartenenti ad altri documenti vengono riportate a `IDStato = 0`
   - tutte le celle del documento `D` vengono portate a `IDStato = 1`
   - il log della picking list resta aggiornato come oggi

Effetto atteso:

- solo il documento `D` risulta prenotato nella griglia
- solo il documento `D` alimenta la coda `F1`
- tutte le altre picking list alimentano `F2`
- premere `Prenota` più volte sulla stessa picking list già prenotata non deve produrre toggle né effetti collaterali

### Caso `S-prenota`

Quando l'operatore s-prenota il documento `D`:

1. se il documento `D` non è prenotato, non succede nulla
2. se il documento `D` è prenotato:
   - le celle del documento `D` vengono riportate a `IDStato = 0`
   - nessun altro documento viene automaticamente prenotato

Effetto atteso:

- nessuna picking list resta prenotata, salvo una nuova prenotazione esplicita
- premere `S-prenota` più volte sulla stessa picking list già non prenotata non deve produrre toggle né effetti collaterali

---

## Strategia tecnica consigliata

### Approccio minimo e prudente

La correzione più sicura, restando vicini al legacy, è mantenere una stored unica ma con una semantica esplicita guidata da un parametro azione.

Proposta:

- stored unica con parametro `@Azione`
  - `P` = Prenota
  - `S` = S-prenota

Logica:

1. determinare se il documento selezionato è già prenotato o no
2. se `@Azione = 'P'`
   - se il documento è già prenotato: non fare nulla
   - altrimenti:
     - azzerare `IDStato = 1` sulle celle coinvolte da altri documenti prenotati
     - impostare `IDStato = 1` sulle celle del documento selezionato
3. se `@Azione = 'S'`
   - se il documento non è prenotato: non fare nulla
   - altrimenti:
     - riportare a `0` solo le celle del documento selezionato

Questa logica mantiene:

- la semantica esistente a livello cella
- ma aggiunge l'esclusività a livello documento
- e mantiene pulita la semantica distinta dei pulsanti `Prenota` e `S-prenota`

### Vantaggi

- modifica contenuta
- comportamento coerente con l'operatività reale
- nessuna necessità immediata di cambiare UI Python o C#
- comportamento idempotente dei pulsanti

---

## Rischi da considerare

### 1. Celle condivise tra documenti

La presenza di celle condivise resta un'anomalia logica del dominio.

Anche con la correzione proposta:

- se una cella appartiene contemporaneamente a più documenti nella vista
- la semantica “quale documento possiede davvero la prenotazione della cella” resta concettualmente debole

Tuttavia la correzione proposta risolve il bug visibile di doppia picking list prenotata.

### 2. Effetti sul flusso barcode

Il barcode usa `IDStato = 1` per la coda `F1`.

Quindi la nuova esclusività deve essere verificata attentamente su:

- proposta UDC in `F1`
- proposta UDC in `F2`
- avanzamento dopo prelievo
- ri-prenotazione automatica residua via `sp_ControllaPrenotazionePackingListPalletNew`

### 3. Interazione con `LogPackingList`

La stored attuale aggiorna il log del documento.

Questa parte va mantenuta, perché il barcode e la logica di ri-prenotazione residua si appoggiano su quel log.

---

## Criteri di accettazione della correzione

La correzione sarà considerata valida se, dopo `Prenota`:

1. una sola picking list risulta prenotata in griglia
2. `F1` propone solo UDC del documento prenotato
3. `F2` propone UDC delle altre picking list
4. il problema di doppia prenotazione non si ripresenta più nemmeno in presenza di celle condivise come `1000` o `8057`

E dopo `S-prenota`:

1. il documento selezionato torna non prenotato
2. `F1` non propone più quella picking list

---

## Decisione consigliata

La correzione lato stored procedure è consigliata e necessaria.

Motivo:

- il bug è reale
- il bug è condiviso da Python e C#
- il bug nasce dalla logica DB attuale
- la prenotazione esclusiva è coerente con il modello operativo del magazzino

---

## Stored procedure proposta

Di seguito una proposta documentale di stored unica con parametro `P` / `S`.

```sql
CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPallet]
    @IDOperatore int,
    @Documento varchar(8),
    @Azione char(1),       -- 'P' = Prenota, 'S' = S-prenota
    @RC int OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET @RC = 0;

    DECLARE @Nominativo varchar(50);
    DECLARE @DocumentoPrenotato bit = 0;
    DECLARE @Description varchar(255) = '';
    DECLARE @Message varchar(255) = '';
    DECLARE @IDResult int = 0;

    SELECT @Nominativo = [Login]
    FROM dbo.Operatori
    WHERE ID = @IDOperatore;

    IF @Azione NOT IN ('P', 'S')
    BEGIN
        SET @RC = -10;
        RETURN;
    END;

    IF OBJECT_ID('tempdb..#TargetCelle') IS NOT NULL DROP TABLE #TargetCelle;
    CREATE TABLE #TargetCelle (
        IDCella int PRIMARY KEY
    );

    INSERT INTO #TargetCelle (IDCella)
    SELECT DISTINCT Cella
    FROM dbo.XMag_ViewPackingList
    WHERE Documento = @Documento
      AND Cella IS NOT NULL;

    IF NOT EXISTS (SELECT 1 FROM #TargetCelle)
    BEGIN
        SET @RC = -20;
        RETURN;
    END;

    IF EXISTS (
        SELECT 1
        FROM dbo.XMag_ViewPackingList
        WHERE Documento = @Documento
          AND ISNULL(IDStato, 0) = 1
    )
    BEGIN
        SET @DocumentoPrenotato = 1;
    END;

    IF @Azione = 'P'
    BEGIN
        -- Gia' prenotata: no-op
        IF @DocumentoPrenotato = 1
            RETURN;

        -- Azzera prenotazioni di altri documenti
        UPDATE c
        SET c.IDStato = 0,
            c.ModUtente = @Nominativo,
            c.ModDataOra = GETDATE()
        FROM dbo.Celle c
        WHERE c.ID IN (
            SELECT DISTINCT Cella
            FROM dbo.XMag_ViewPackingList
            WHERE ISNULL(IDStato, 0) = 1
              AND Documento <> @Documento
              AND Cella IS NOT NULL
        );

        -- Prenota solo il documento target
        UPDATE c
        SET c.IDStato = 1,
            c.ModUtente = @Nominativo,
            c.ModDataOra = GETDATE()
        FROM dbo.Celle c
        INNER JOIN #TargetCelle t ON t.IDCella = c.ID;

        SELECT TOP 1 @Description = NAZIONE
        FROM dbo.XMag_ViewPackingList
        WHERE Documento = @Documento;

        EXEC dbo.sp_LogPackingList
             @ID = 0,
             @Code = @Documento,
             @Description = @Description,
             @Message = @Message OUTPUT,
             @IDResult = @IDResult OUTPUT;

        RETURN;
    END;

    IF @Azione = 'S'
    BEGIN
        -- Gia' non prenotata: no-op
        IF @DocumentoPrenotato = 0
            RETURN;

        -- S-prenota solo il documento target
        UPDATE c
        SET c.IDStato = 0,
            c.ModUtente = @Nominativo,
            c.ModDataOra = GETDATE()
        FROM dbo.Celle c
        INNER JOIN #TargetCelle t ON t.IDCella = c.ID;

        RETURN;
    END;
END;
```

### Note sulla stored proposta

- Non esiste più il toggle implicito.
- `Prenota` è idempotente:
  - se la list è già prenotata, non cambia nulla.
- `S-prenota` è idempotente:
  - se la list è già non prenotata, non cambia nulla.
- L'esclusività viene garantita solo nel ramo `P`.
- La logica continua a usare `IDStato` sulle celle, così il barcode legacy può continuare a usare `F1` / `F2` con la semantica attuale.

---

## Prossimo passo

Dopo approvazione di questo documento:

1. modificare `sp_xExePackingListPallet` o introdurre una nuova versione compatibile
2. testare il risultato con almeno 3 picking list attive
3. verificare il comportamento sul backoffice
4. verificare il comportamento sul barcode (`F1` / `F2`)

---

## Revisione dopo test reale su documenti 133 / 135 / 137

Dopo avere applicato la prima correzione proposta solo sulla stored, il bug **resta presente**:

- prenotando il documento `133`
- la picking list `135` continua a risultare prenotata

Questo comportamento è coerente con le evidenze raccolte:

- `133` e `135` condividono la cella `8057`
- `135` e `137` condividono la cella `1000`

### Conclusione aggiornata

La correzione che agisce **solo** su `Celle.IDStato` non è sufficiente.

Il motivo è strutturale:

1. la stored può anche rendere idempotente `Prenota` / `S-prenota`
2. ma se la UI e il barcode continuano a dedurre lo stato della prenotazione da `Celle.IDStato`
3. una cella condivisa continuerà a far risultare prenotati anche documenti diversi

Quindi il modello corretto deve spostare la **sorgente di verità** della prenotazione:

- **da:** stato della cella
- **a:** documento prenotato attivo

### Nuova architettura proposta

La soluzione corretta è questa:

1. introdurre una piccola tabella di stato, ad esempio `dbo.PickingListReservation`
2. salvare lì il **documento attualmente prenotato**
3. fare in modo che `XMag_ViewPackingList.IDStato` non legga più la prenotazione da `Celle.IDStato`
4. calcolare invece `IDStato` così:
   - `1` se `Documento = documento prenotato attivo`
   - `0` negli altri casi

In questo modo:

- il backoffice mostra prenotata una sola picking list
- il barcode continua a usare `F1` = `IDStato 1` e `F2` = `IDStato 0`
- le celle condivise `1000` / `8057` non contaminano più altri documenti

### Ruolo residuo di `Celle.IDStato`

`Celle.IDStato` può restare utile come supporto:

- per visualizzazioni legacy
- per evidenziare le celle del documento attivo
- per non rompere altre parti del sistema che si aspettano quel flag

Ma non deve più essere la **fonte primaria** della prenotazione documento.

### Stored definitiva da realizzare

La stored `sp_xExePackingListPallet` deve quindi:

1. mantenere il parametro `@Azione = 'P' | 'S'`
2. aggiornare la tabella `PickingListReservation`
3. opzionalmente riallineare anche `Celle.IDStato`
4. mantenere il log in `LogPackingList`

Semantica definitiva:

- `Prenota`
  - se il documento è già quello attivo: nessun effetto
  - altrimenti:
    - il documento diventa l’unico prenotato attivo
    - tutte le altre picking list risultano non prenotate

- `S-prenota`
  - se il documento non è quello attivo: nessun effetto
  - se il documento è quello attivo:
    - la prenotazione viene rimossa
    - nessun altro documento viene acceso automaticamente

### Nota importante

Questa revisione **supera** la proposta precedente basata soltanto sul reset/set di `Celle.IDStato`.

La patch SQL definitiva deve quindi includere:

1. creazione della tabella `PickingListReservation`
2. `CREATE OR ALTER` della stored `sp_xExePackingListPallet`
3. `CREATE OR ALTER` della vista `XMag_ViewPackingList`

Questo è il primo assetto realmente coerente con il requisito di business:

> una sola picking list prenotata alla volta, anche in presenza di celle condivise.

---

## Evoluzione proposta: usare `ViewPackingListRestante` anche per la lista alta

### Situazione attuale

Nel legacy C# e nel Python attuale la distinzione è questa:

- la **griglia alta** legge la testata aggregata da `XMag_ViewPackingList`
- la **griglia bassa** legge il dettaglio da `ViewPackingListRestante`

La vista `ViewPackingListRestante` è definita come:

```sql
SELECT ...
FROM XMag_ViewPackingList
WHERE Cella <> 9999
```

Quindi:

- `XMag_ViewPackingList` contiene anche le UDC già finite nella locazione convenzionale `9999 = 7G.1.1`
- `ViewPackingListRestante` mostra invece solo le UDC ancora residue

### Conseguenza pratica

All’istante `t = 0` della prenotazione:

- se la picking list non è ancora stata lavorata, `XMag_ViewPackingList` e `ViewPackingListRestante` mostrano di fatto lo stesso insieme di UDC
- le UDC non scaffalate **restano presenti** in entrambe, perché stanno nella cella convenzionale `1000 = 5E1.1`

Dopo che il magazziniere ha iniziato i prelievi:

- le UDC già spedite/prelevate finiscono in `9999 = 7G.1.1`
- quindi spariscono da `ViewPackingListRestante`
- ma restano ancora visibili in `XMag_ViewPackingList`

### Limite del comportamento attuale

Con la summary alta basata su `XMag_ViewPackingList`:

- il comando `Ricarica` può continuare a mostrare picking list già lavorate in parte
- una picking list completamente esaurita può restare visibile nella lista alta
- il contenuto della lista alta non coincide più con il “residuo operativo reale”

In altre parole:

- la griglia bassa mostra il residuo
- la griglia alta mostra ancora lo storico completo

### Modifica proposta

La proposta è:

- usare `ViewPackingListRestante` anche come sorgente della **lista alta aggregata**

quindi sostituire, nella query summary di [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py):

- `FROM dbo.XMag_ViewPackingList`

con:

- `FROM dbo.ViewPackingListRestante`

mantenendo invariati:

- `GROUP BY Documento, CodNazione, NAZIONE, Stato`
- `COUNT(DISTINCT Pallet)`
- `COUNT(DISTINCT Lotto)`
- `COUNT(DISTINCT Articolo)`
- `SUM(Qta)`
- `MIN(Ordinamento)`
- `MAX(IDStato)`

### Effetti attesi

Con questa modifica:

1. all’inizio una picking list nuova continua a comparire normalmente
2. durante il lavoro il pulsante `Ricarica` mostra solo ciò che resta davvero da prelevare
3. le UDC già finite in `7G.1.1` non inquinano più i conteggi della testata
4. una picking list completamente esaurita esce naturalmente dalla lista visibile
5. le UDC non scaffalate continuano a comparire correttamente, perché stanno in `1000 = 5E1.1`, non in `9999`

### Chiusura automatica della prenotazione

Per mantenere coerenti:

- griglia alta
- stato della prenotazione
- comportamento del barcode `F1`

la prenotazione attiva non deve soltanto sparire dalla griglia: deve essere anche **azzerata logicamente** quando il residuo arriva a zero.

La patch SQL definitiva quindi deve fare anche questo:

- dopo ogni movimento, controllare se il documento prenotato ha ancora righe in `ViewPackingListRestante`
- se non ne ha più:
  - la prenotazione attiva va portata a `NULL` / `0`
  - le eventuali celle residue marcate `IDStato = 1` vanno riportate a `0`

Effetto operativo:

- plist piena all’inizio
- plist via via ridotta durante i prelievi
- plist che, all’ultima UDC, sparisce dalla griglia alta
- e nello stesso momento cessa anche di essere la coda `F1`

### Impatto funzionale

Questa modifica:

- **non è** una replica pedissequa del legacy C#
- ma è un miglioramento operativo coerente con il comportamento atteso dal magazzino

In particolare rende la lista alta:

- più coerente con il significato di “picking list ancora da esaurire”
- allineata al contenuto della griglia bassa
- più utile nei refresh successivi durante il lavoro reale

### Impatto tecnico

Il cambiamento richiesto è piccolo lato Python:

- aggiornare la query summary in [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py)

Non richiede, in prima battuta:

- modifiche alla stored di prenotazione
- modifiche al barcode
- modifiche a `ViewPackingListRestante`

### Decisione progettuale

Questa modifica va considerata come una scelta esplicita di evoluzione:

- **se vogliamo restare 1:1 col C#**, la lista alta resta su `XMag_ViewPackingList`
- **se vogliamo rendere il modulo più aderente al residuo operativo reale**, conviene passare a `ViewPackingListRestante`
