22 KiB
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:
- seleziona una picking list
- preme
Prenota - 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
F1sul barcode - le altre picking list restano non prenotate e alimentano la coda
F2
Comportamento attuale
Il comportamento effettivamente osservato è questo:
- l'operatore seleziona una sola picking list
- preme
Prenota - 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 chiama:
che esegue direttamente la stored procedure SQL legacy:
dbo.sp_xExePackingListPallet
Lato C#
La finestra legacy 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 aggrega così:
GROUP BY Documento, CodNazione, NAZIONE, StatoMAX(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:
Logica attuale
La stored:
- legge da
XMag_ViewPackingList - estrae le
Cellaassociate alDocumento - per ogni cella:
- se
IDStato = 0, la mette a1 - altrimenti la rimette a
0
- se
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 = 1sulla 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:
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:
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:
135 1000 16 16
137 1000 2 1
133 8057 1 1
135 8057 1 1
Interpretazione:
- documento
135e documento137condividono la cella1000 - documento
133e documento135condividono la cella8057
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:
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:
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:
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:
135 1000 16 16
137 1000 2 1
133 8057 1 1
135 8057 1 1
Interpretazione:
- documento
135e documento137condividono la cella1000 - documento
133e documento135condividono la cella8057
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.19999=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:
- la stored
sp_xExePackingListPalletlavora a livello cella - la UI mostra la prenotazione a livello documento
Quando esistono celle condivise tra documenti:
- la prenotazione di un documento produce
IDStato = 1anche 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:
- rimuovere la prenotazione da tutte le altre picking list
- 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:
- se il documento
Dè già prenotato, non succede nulla - se il documento
Dnon è prenotato:- tutte le celle prenotate appartenenti ad altri documenti vengono riportate a
IDStato = 0 - tutte le celle del documento
Dvengono portate aIDStato = 1 - il log della picking list resta aggiornato come oggi
- tutte le celle prenotate appartenenti ad altri documenti vengono riportate a
Effetto atteso:
- solo il documento
Drisulta prenotato nella griglia - solo il documento
Dalimenta la codaF1 - tutte le altre picking list alimentano
F2 - premere
Prenotapiù 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:
- se il documento
Dnon è prenotato, non succede nulla - se il documento
Dè prenotato:- le celle del documento
Dvengono riportate aIDStato = 0 - nessun altro documento viene automaticamente prenotato
- le celle del documento
Effetto atteso:
- nessuna picking list resta prenotata, salvo una nuova prenotazione esplicita
- premere
S-prenotapiù 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
@AzioneP= PrenotaS= S-prenota
Logica:
- determinare se il documento selezionato è già prenotato o no
- se
@Azione = 'P'- se il documento è già prenotato: non fare nulla
- altrimenti:
- azzerare
IDStato = 1sulle celle coinvolte da altri documenti prenotati - impostare
IDStato = 1sulle celle del documento selezionato
- azzerare
- se
@Azione = 'S'- se il documento non è prenotato: non fare nulla
- altrimenti:
- riportare a
0solo le celle del documento selezionato
- riportare a
Questa logica mantiene:
- la semantica esistente a livello cella
- ma aggiunge l'esclusività a livello documento
- e mantiene pulita la semantica distinta dei pulsanti
PrenotaeS-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:
- una sola picking list risulta prenotata in griglia
F1propone solo UDC del documento prenotatoF2propone UDC delle altre picking list- il problema di doppia prenotazione non si ripresenta più nemmeno in presenza di celle condivise come
1000o8057
E dopo S-prenota:
- il documento selezionato torna non prenotato
F1non 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.
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
IDStatosulle celle, così il barcode legacy può continuare a usareF1/F2con la semantica attuale.
Prossimo passo
Dopo approvazione di questo documento:
- modificare
sp_xExePackingListPalleto introdurre una nuova versione compatibile - testare il risultato con almeno 3 picking list attive
- verificare il comportamento sul backoffice
- 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
135continua a risultare prenotata
Questo comportamento è coerente con le evidenze raccolte:
133e135condividono la cella8057135e137condividono la cella1000
Conclusione aggiornata
La correzione che agisce solo su Celle.IDStato non è sufficiente.
Il motivo è strutturale:
- la stored può anche rendere idempotente
Prenota/S-prenota - ma se la UI e il barcode continuano a dedurre lo stato della prenotazione da
Celle.IDStato - 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:
- introdurre una piccola tabella di stato, ad esempio
dbo.PickingListReservation - salvare lì il documento attualmente prenotato
- fare in modo che
XMag_ViewPackingList.IDStatonon legga più la prenotazione daCelle.IDStato - calcolare invece
IDStatocosì:1seDocumento = documento prenotato attivo0negli altri casi
In questo modo:
- il backoffice mostra prenotata una sola picking list
- il barcode continua a usare
F1=IDStato 1eF2=IDStato 0 - le celle condivise
1000/8057non 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:
- mantenere il parametro
@Azione = 'P' | 'S' - aggiornare la tabella
PickingListReservation - opzionalmente riallineare anche
Celle.IDStato - 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:
- creazione della tabella
PickingListReservation CREATE OR ALTERdella storedsp_xExePackingListPalletCREATE OR ALTERdella vistaXMag_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:
SELECT ...
FROM XMag_ViewPackingList
WHERE Cella <> 9999
Quindi:
XMag_ViewPackingListcontiene anche le UDC già finite nella locazione convenzionale9999 = 7G.1.1ViewPackingListRestantemostra invece solo le UDC ancora residue
Conseguenza pratica
All’istante t = 0 della prenotazione:
- se la picking list non è ancora stata lavorata,
XMag_ViewPackingListeViewPackingListRestantemostrano 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
Ricaricapuò 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
ViewPackingListRestanteanche come sorgente della lista alta aggregata
quindi sostituire, nella query summary di gestione_pickinglist.py:
FROM dbo.XMag_ViewPackingList
con:
FROM dbo.ViewPackingListRestante
mantenendo invariati:
GROUP BY Documento, CodNazione, NAZIONE, StatoCOUNT(DISTINCT Pallet)COUNT(DISTINCT Lotto)COUNT(DISTINCT Articolo)SUM(Qta)MIN(Ordinamento)MAX(IDStato)
Effetti attesi
Con questa modifica:
- all’inizio una picking list nuova continua a comparire normalmente
- durante il lavoro il pulsante
Ricaricamostra solo ciò che resta davvero da prelevare - le UDC già finite in
7G.1.1non inquinano più i conteggi della testata - una picking list completamente esaurita esce naturalmente dalla lista visibile
- le UDC non scaffalate continuano a comparire correttamente, perché stanno in
1000 = 5E1.1, non in9999
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 = 1vanno riportate a0
- la prenotazione attiva va portata a
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
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