From 3a2893dc3da5036025a4a153a984e19b9af47f55 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Tue, 29 Sep 2026 11:52:33 +0200 Subject: [PATCH] Aggiungi sequenza prelievo e gestisci destinazioni senza nazione --- ...si_fix_pickinglist_nazione_destinazione.md | 247 ++++++++ apply_online_python_wms_full_patch.sql | 198 +++++- apply_pickinglist_days_filter_patch.sql | 190 ++++++ ...ist_optional_destination_country_patch.sql | 217 +++++++ apply_pickinglist_sequence_patch.sql | 136 ++++ barcode_repository.py | 8 +- gestione_pickinglist.py | 88 ++- locale.json | 46 ++ ordinamento.md | 414 +++++++++++++ pickinglist_sequence.py | 584 ++++++++++++++++++ rollback_pickinglist_days_filter_patch.sql | 76 +++ rollback_pickinglist_sequence_patch.sql | 19 + specifica_sequenza_prelievo.md | 74 +++ tooltip.json | 20 +- ui_theme.json | 23 + user_session.py | 1 + version_info.py | 7 +- 17 files changed, 2329 insertions(+), 19 deletions(-) create mode 100644 analisi_fix_pickinglist_nazione_destinazione.md create mode 100644 apply_pickinglist_days_filter_patch.sql create mode 100644 apply_pickinglist_optional_destination_country_patch.sql create mode 100644 apply_pickinglist_sequence_patch.sql create mode 100644 ordinamento.md create mode 100644 pickinglist_sequence.py create mode 100644 rollback_pickinglist_days_filter_patch.sql create mode 100644 rollback_pickinglist_sequence_patch.sql create mode 100644 specifica_sequenza_prelievo.md diff --git a/analisi_fix_pickinglist_nazione_destinazione.md b/analisi_fix_pickinglist_nazione_destinazione.md new file mode 100644 index 0000000..44d810d --- /dev/null +++ b/analisi_fix_pickinglist_nazione_destinazione.md @@ -0,0 +1,247 @@ +# Picking list con nazione di destinazione mancante + +## Scopo + +Questo documento descrive il difetto che rendeva invisibile una picking list +quando il documento SAM utilizzava un indirizzo di destinazione alternativo +senza nazione valorizzata. Descrive inoltre la correzione applicata al solo +ramo dati Python del WMS, le conseguenze funzionali e le verifiche da eseguire. + +La correzione non modifica tabelle o viste legacy utilizzate dal programma C# +e non aggiorna dati anagrafici o documentali in SAM. + +## Caso che ha evidenziato il difetto + +Il problema e' stato rilevato sulla picking list `244/2026`: + +| Dato | Valore | +|---|---| +| `BAMTES.ID` | `11896` | +| `BAMTES.NUMDOC` | `244` | +| `BAMTES.ANNDOC` | `2026` | +| Data documento | `24/09/2026` | +| `BAMTES.STATO` | `P` | +| `BAMTES.IDINDDEST` | `274` | +| Destinazione | `c/o UNITRANS Magazzino 4 Mirandola` | +| `BAMTES.IDNAZDEST` | `NULL` | + +L'indirizzo alternativo `SAMA1.dbo.INDANA.ID = 274` esiste, ma anche il suo +campo `IDNAZIONI` e' `NULL`. L'indirizzo abituale della stessa controparte ha +invece `IDNAZIONI = 21`, corrispondente all'Italia. + +Il documento non aveva altri problemi: + +- anno corrente valido; +- stato `P` valido; +- data compresa nell'intervallo operativo; +- 4 righe documento; +- 494 righe lotto valide; +- 26 UDC distinte; +- trasportatore esistente. + +## Causa tecnica + +La preparazione delle picking list collegava la testata del documento alla +tabella delle nazioni con un collegamento obbligatorio: + +```sql +FROM SAMA1.dbo.NAZIONI +INNER JOIN SAMA1.dbo.BAMTES + ON SAMA1.dbo.NAZIONI.ID = SAMA1.dbo.BAMTES.IDNAZDEST +``` + +Un `INNER JOIN` conserva soltanto le testate per le quali esiste una nazione +compatibile. Quando `BAMTES.IDNAZDEST` e' `NULL`, il confronto non puo' trovare +alcuna riga e l'intero documento scompare prima che vengano elaborate righe, +lotti e UDC. + +Nel caso 244 la diagnosi ha prodotto: + +```text +Righe valide senza join obbligatorio a NAZIONI: 494 +Righe valide con join obbligatorio a NAZIONI: 0 +``` + +Il filtro della GUI a 10 giorni, la prenotazione e la sequenza di prelievo non +erano quindi responsabili del problema. + +## Flusso dati interessato + +La pagina Gestione Picking List usa questa catena: + +```text +SAMA1.dbo.BAMTES / FATRIG / LOTTIBF / LOTSER + | + v +dbo.py_XMag_ViewPackingList + | + v +dbo.py_ViewPackingListRestante + | + v +dbo.py_ViewPackingListPrelievo + | + v +Gestione Picking List e barcode Python +``` + +Lo storico Python utilizza inoltre: + +```text +SAMA1 + -> dbo.py_vPreparaPackingListSAMA1 + -> dbo.py_vPreparaPackingList + -> dbo.py_XMag_ViewPackingListStorico +``` + +Per evitare comportamenti diversi fra operativita' e storico, la correzione e' +stata applicata sia alla preparazione operativa sia alla preparazione storica. + +## Correzione applicata + +Il collegamento e' stato reso opzionale: + +```sql +FROM SAMA1.dbo.BAMTES AS bt +LEFT OUTER JOIN SAMA1.dbo.NAZIONI AS n + ON n.ID = bt.IDNAZDEST +``` + +Con il `LEFT OUTER JOIN` la testata BAMTES viene conservata anche quando la +nazione e' assente. Gli altri collegamenti essenziali verso righe, lotti e +seriali restano obbligatori. + +Sono state modificate soltanto queste viste Python: + +- `dbo.py_vPreparaPackingListSAMA1`; +- `dbo.py_XMag_ViewPackingList`. + +Le viste dipendenti ricevono automaticamente i dati corretti: + +- `dbo.py_vPreparaPackingList`; +- `dbo.py_XMag_ViewPackingListStorico`; +- `dbo.py_ViewPackingListRestante`; +- `dbo.py_ViewPackingListPrelievo`. + +## Gestione dei valori mancanti + +La patch non assegna automaticamente una nazione. Questo evita di trasformare +un'anagrafica incompleta in un dato apparentemente certo. + +Quando la nazione manca: + +- `CodNazione` resta `NULL`; +- `NAZIONE` resta `NULL`; +- il documento e le sue UDC restano visibili; +- il `CASE` applicativo gia' esistente usa il ramo generico `Stato = 4`; +- prenotazione, dettaglio UDC e sequenza di prelievo continuano a funzionare. + +La correzione dell'anagrafica SAM rimane comunque consigliata. La robustezza +della vista impedisce pero' che un campo descrittivo incompleto blocchi un +processo operativo di magazzino. + +## Script di installazione + +La patch autonoma e' contenuta in: + +```text +apply_pickinglist_optional_destination_country_patch.sql +``` + +Lo stesso cambiamento e' stato integrato anche negli script cumulativi: + +- `apply_pickinglist_days_filter_patch.sql`; +- `apply_online_python_wms_full_patch.sql`. + +Per aggiornare un database che possiede gia' le viste Python e' sufficiente +eseguire una sola volta in SSMS la patch autonoma. Gli script usano +`CREATE OR ALTER VIEW`, quindi possono essere rieseguiti senza duplicare dati. + +## Verifica post-patch + +Controllo della vista operativa: + +```sql +SELECT + Documento, + DataDocumento, + CodNazione, + NAZIONE, + Stato, + IDStato, + COUNT(DISTINCT Pallet) AS NumeroUDC +FROM dbo.py_ViewPackingListPrelievo +WHERE Documento = 244 +GROUP BY + Documento, + DataDocumento, + CodNazione, + NAZIONE, + Stato, + IDStato; +``` + +Risultato ottenuto dopo la correzione: + +```text +Documento: 244 +CodNazione: NULL +NAZIONE: NULL +Stato: 4 +IDStato: 0 +Numero UDC: 26 +``` + +Controllo dei quattro livelli principali: + +```sql +SELECT COUNT(DISTINCT UDC) AS UDC +FROM dbo.py_vPreparaPackingListSAMA1 +WHERE NUMDOC = 244 AND StatoDocumento = 'P'; + +SELECT COUNT(DISTINCT Pallet) AS UDC +FROM dbo.py_XMag_ViewPackingList +WHERE Documento = 244; + +SELECT COUNT(DISTINCT Pallet) AS UDC +FROM dbo.py_ViewPackingListRestante +WHERE Documento = 244; + +SELECT COUNT(DISTINCT Pallet) AS UDC +FROM dbo.py_ViewPackingListPrelievo +WHERE Documento = 244; +``` + +Tutte e quattro le query devono restituire `26` per il caso analizzato. + +## Impatto e compatibilita' + +- Nessuna riga SAM viene inserita, aggiornata o cancellata. +- Nessuna prenotazione viene modificata. +- Nessuna stored procedure di movimentazione viene modificata. +- Le picking list con nazione valida mantengono lo stesso risultato. +- Le picking list prima invisibili per nazione nulla diventano visibili. +- Gli oggetti legacy senza prefisso `py_` non vengono modificati. + +## Ripristino concettuale + +Per ripristinare il comportamento precedente occorrerebbe riportare il +collegamento `BAMTES -> NAZIONI` da `LEFT OUTER JOIN` a `INNER JOIN` nelle due +viste Python modificate. Tale rollback farebbe nuovamente scomparire tutti i +documenti con `IDNAZDEST` nullo e non e' quindi raccomandato come soluzione +operativa. + +## Miglioramento futuro consigliato + +Una diagnostica separata potrebbe mostrare le picking list con dati anagrafici +incompleti, senza impedirne l'uso: + +```sql +SELECT ID, ANNDOC, NUMDOC, IDINDDEST, DESCRDEST +FROM SAMA1.dbo.BAMTES +WHERE STATO = 'P' + AND IDNAZDEST IS NULL; +``` + +In questo modo il WMS rimane operativo e l'amministratore puo' correggere +l'anagrafica SAM in un secondo momento. diff --git a/apply_online_python_wms_full_patch.sql b/apply_online_python_wms_full_patch.sql index 0093231..590e6c1 100644 --- a/apply_online_python_wms_full_patch.sql +++ b/apply_online_python_wms_full_patch.sql @@ -16,6 +16,14 @@ Include: - dbo.py_vPreparaPackingList - dbo.py_XMag_ViewPackingListStorico - dbo.py_BarcodePickingListSkip +- dbo.py_PickingListPrelievoSequenza +- dbo.py_ViewPackingListPrelievo + +Include inoltre: +- intervallo operativo massimo di 30 giorni, filtrato dalla UI a 10/15/20/30; +- destinazione senza nazione ammessa nel ramo Python mediante LEFT JOIN; +- ordine predefinito con UDC Non scaff. prima degli scaffali; +- ordine personalizzabile per singolo Documento. Nota storico: dbo.py_vPreparaPackingListSAMA1 usa ANNDOC >= YEAR(GETDATE()) @@ -362,8 +370,8 @@ SELECT SAMA1.dbo.NAZIONI.CODICE AS Expr1, SAMA1.dbo.MTRASP.CODICE + ' ' + SAMA1.dbo.NAZIONI.DESCR AS NAZIONE, SAMA1.dbo.BAMTES.IDMTRASP -FROM SAMA1.dbo.NAZIONI -INNER JOIN SAMA1.dbo.BAMTES +FROM SAMA1.dbo.BAMTES +LEFT OUTER JOIN SAMA1.dbo.NAZIONI ON SAMA1.dbo.NAZIONI.ID = SAMA1.dbo.BAMTES.IDNAZDEST RIGHT OUTER JOIN SAMA1.dbo.LOTTIBF LEFT OUTER JOIN SAMA1.dbo.LOTSER @@ -527,5 +535,191 @@ BEGIN END; GO +/* ============================================================ + 6. Intervallo operativo 30 giorni e sequenza di prelievo + + La vista di preparazione Python creata al punto 4 contiene sia P sia D. + Qui il ramo operativo viene ristretto alle sole liste P degli ultimi 30 + giorni; la UI applica successivamente il valore scelto (10/15/20/30). + ============================================================ */ + +CREATE OR ALTER VIEW dbo.py_XMag_ViewPackingList +AS +SELECT TOP (10000) + prep.UDC AS Pallet, + prep.NUMLOT AS Lotto, + prep.CODICE AS Articolo, + prep.DESCR AS Descrizione, + prep.Qta, + prep.NUMDOC AS Documento, + prep.DATDOC AS DataDocumento, + prep.Expr1 AS CodNazione, + prep.NAZIONE, + CASE + WHEN prep.Expr1 = 'DE' THEN 10 + WHEN prep.Expr1 = 'TH' THEN + CASE WHEN SUBSTRING(prep.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END + WHEN prep.Expr1 = 'MEX' THEN 12 + ELSE 4 + END AS Stato, + ISNULL(g.NumeroPallet, 0) AS PalletCella, + ISNULL(g.IDMagazzino, 1) AS Magazzino, + ISNULL(g.IDArea, 5) AS Area, + ISNULL(g.IDCella, 1000) AS Cella, + ISNULL(c.Ordinamento, 99999) AS Ordinamento, + ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione, + SUBSTRING(prep.DESCRDEST, 1, 2) AS DEST, + CASE + WHEN MAX(CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(prep.NUMDOC AS varchar(8)) + THEN 1 ELSE 0 END) = 1 + THEN 1 + ELSE 0 + END AS IDStato +FROM dbo.Celle AS c +INNER JOIN dbo.XMag_GiacenzaPallet AS g + ON c.ID = g.IDCella +RIGHT OUTER JOIN dbo.py_vPreparaPackingList AS prep + ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC +LEFT JOIN dbo.PyPickingListReservation AS pr + ON pr.ID = 1 + AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL +WHERE prep.StatoDocumento = 'P' + AND prep.DATDOC > CAST((GETDATE() - 30) AS int) +GROUP BY + prep.Expr1, + prep.NAZIONE, + prep.UDC, + prep.NUMDOC, + prep.DATDOC, + prep.NUMLOT, + prep.CODICE, + prep.DESCR, + prep.Qta, + g.NumeroPallet, + g.IDMagazzino, + g.IDArea, + g.IDCella, + c.Ordinamento, + c.Corsia, + c.Colonna, + c.Fila, + prep.DESCRDEST; +GO + +CREATE OR ALTER VIEW dbo.py_ViewPackingListRestante +AS +SELECT + Pallet, Lotto, Articolo, Descrizione, Qta, Documento, DataDocumento, + CodNazione, NAZIONE, Stato, PalletCella, Magazzino, Area, Cella, + Ordinamento, Ubicazione, DEST, IDStato +FROM dbo.py_XMag_ViewPackingList +WHERE Cella <> 9999; +GO + +IF OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') IS NULL +BEGIN + CREATE TABLE dbo.py_PickingListPrelievoSequenza ( + ID bigint IDENTITY(1,1) NOT NULL + CONSTRAINT PK_py_PickingListPrelievoSequenza PRIMARY KEY, + Documento varchar(50) NOT NULL, + GruppoChiave varchar(160) NOT NULL, + GruppoEtichetta varchar(100) NOT NULL, + Ordine int NOT NULL, + InsUtente varchar(50) NULL, + InsDataOra datetime2(0) NOT NULL + CONSTRAINT DF_py_PickingListPrelievoSequenza_InsDataOra DEFAULT SYSDATETIME(), + ModUtente varchar(50) NULL, + ModDataOra datetime2(0) NULL, + DelUtente varchar(50) NULL, + DelDataOra datetime2(0) NULL, + CONSTRAINT CK_py_PickingListPrelievoSequenza_Ordine CHECK (Ordine >= 0) + ); +END; +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') + AND name = N'UX_py_PickingListPrelievoSequenza_DocGruppo' +) +BEGIN + CREATE UNIQUE INDEX UX_py_PickingListPrelievoSequenza_DocGruppo + ON dbo.py_PickingListPrelievoSequenza (Documento, GruppoChiave) + WHERE DelDataOra IS NULL; +END; +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') + AND name = N'UX_py_PickingListPrelievoSequenza_DocOrdine' +) +BEGIN + CREATE UNIQUE INDEX UX_py_PickingListPrelievoSequenza_DocOrdine + ON dbo.py_PickingListPrelievoSequenza (Documento, Ordine) + WHERE DelDataOra IS NULL; +END; +GO + +CREATE OR ALTER VIEW dbo.py_ViewPackingListPrelievo +AS +WITH Gruppi AS ( + SELECT + pl.*, + CASE + WHEN pl.Cella = 1000 THEN 'NON_SCAFF' + ELSE CONCAT( + 'M:', pl.Magazzino, '|A:', pl.Area, '|C:', + LTRIM(RTRIM(CASE + WHEN CHARINDEX(' - ', pl.Ubicazione) > 0 + THEN LEFT(pl.Ubicazione, CHARINDEX(' - ', pl.Ubicazione) - 1) + ELSE pl.Ubicazione + END)) + ) + END AS GruppoPrelievoChiave, + CASE + WHEN pl.Cella = 1000 THEN 'Non scaff.' + ELSE LTRIM(RTRIM(CASE + WHEN CHARINDEX(' - ', pl.Ubicazione) > 0 + THEN LEFT(pl.Ubicazione, CHARINDEX(' - ', pl.Ubicazione) - 1) + ELSE pl.Ubicazione + END)) + END AS GruppoPrelievo, + CASE + WHEN pl.Cella = 1000 THEN 0 + ELSE 1000 + ISNULL(pl.Ordinamento, 99999) + END AS OrdineGruppoDefault + FROM dbo.py_ViewPackingListRestante AS pl +), Sequenza AS ( + SELECT + g.*, + s.Ordine AS OrdineGruppoConfigurato, + CASE WHEN EXISTS ( + SELECT 1 + FROM dbo.py_PickingListPrelievoSequenza AS any_s + WHERE any_s.Documento COLLATE Latin1_General_CI_AS = + CAST(g.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS + AND any_s.DelDataOra IS NULL + ) THEN 1 ELSE 0 END AS HaSequenzaPersonalizzata + FROM Gruppi AS g + LEFT JOIN dbo.py_PickingListPrelievoSequenza AS s + ON s.Documento COLLATE Latin1_General_CI_AS = + CAST(g.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS + AND s.GruppoChiave COLLATE Latin1_General_CI_AS = + g.GruppoPrelievoChiave COLLATE Latin1_General_CI_AS + AND s.DelDataOra IS NULL +) +SELECT + Sequenza.*, + CASE + WHEN OrdineGruppoConfigurato IS NOT NULL THEN OrdineGruppoConfigurato + WHEN HaSequenzaPersonalizzata = 1 THEN 100000 + OrdineGruppoDefault + ELSE OrdineGruppoDefault + END AS OrdinePrelievoGruppo +FROM Sequenza; +GO + PRINT 'Patch cumulativa Python WMS completata.'; GO diff --git a/apply_pickinglist_days_filter_patch.sql b/apply_pickinglist_days_filter_patch.sql new file mode 100644 index 0000000..d642528 --- /dev/null +++ b/apply_pickinglist_days_filter_patch.sql @@ -0,0 +1,190 @@ +/* +Patch: intervallo giorni selezionabile in Gestione Picking List Python + +Scopo: +- permettere alla UI Python di caricare picking list operative degli ultimi + 10, 15, 20 o 30 giorni; +- mantenere 10 giorni come default applicativo; +- non escludere documenti validi con nazione di destinazione non valorizzata; +- non modificare gli oggetti legacy usati dal programma C#. + +Oggetti modificati: +- dbo.py_XMag_ViewPackingList +- dbo.py_ViewPackingListRestante + +Nota: +La vista legacy dbo.XMag_ViewPackingList dipende da dbo.vPreparaPackingListSAMA1, +che contiene il filtro fisso GETDATE()-10. Per questo la vista Python viene +ricalcolata direttamente dalle tabelle SAM con un orizzonte massimo di 30 giorni. +La UI applica poi il filtro effettivo scelto dall'operatore. +Il collegamento a NAZIONI e' intenzionalmente LEFT OUTER JOIN: un indirizzo +alternativo incompleto non deve rendere invisibile l'intera picking list. +*/ + +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingList] +AS +WITH PreparaPackingList AS ( + SELECT + SAMA1.dbo.LOTSER.NUMLOT, + SAMA1.dbo.ARTICO.CODICE, + SAMA1.dbo.FATRIG.DESCR, + LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) AS UDC, + SUM(SAMA1.dbo.LOTSER.QTTGIAI) AS Qta, + SAMA1.dbo.BAMTES.NUMDOC, + SAMA1.dbo.BAMTES.DATDOC, + SAMA1.dbo.BAMTES.ID, + SAMA1.dbo.BAMTES.DESCRDEST, + SAMA1.dbo.NAZIONI.CODICE AS Expr1, + SAMA1.dbo.MTRASP.CODICE + ' ' + SAMA1.dbo.NAZIONI.DESCR AS NAZIONE, + SAMA1.dbo.BAMTES.IDMTRASP + FROM SAMA1.dbo.BAMTES + LEFT OUTER JOIN SAMA1.dbo.NAZIONI + ON SAMA1.dbo.NAZIONI.ID = SAMA1.dbo.BAMTES.IDNAZDEST + RIGHT OUTER JOIN SAMA1.dbo.LOTTIBF + LEFT OUTER JOIN SAMA1.dbo.LOTSER + ON SAMA1.dbo.LOTTIBF.IDLOTSER = SAMA1.dbo.LOTSER.ID + LEFT OUTER JOIN SAMA1.dbo.ARTICO + ON SAMA1.dbo.LOTSER.IDARTICO = SAMA1.dbo.ARTICO.ID + LEFT OUTER JOIN SAMA1.dbo.FATRIG + ON SAMA1.dbo.LOTTIBF.IDFATRIG = SAMA1.dbo.FATRIG.ID + ON SAMA1.dbo.BAMTES.ID = SAMA1.dbo.FATRIG.IDBAM + LEFT OUTER JOIN SAMA1.sam.EXTUC + ON LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) = SAMA1.sam.EXTUC.CODICE + LEFT OUTER JOIN SAMA1.dbo.MTRASP + ON SAMA1.dbo.BAMTES.IDMTRASP = SAMA1.dbo.MTRASP.ID + WHERE SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE()) + AND SAMA1.dbo.BAMTES.STATO = 'P' + AND SAMA1.dbo.BAMTES.DATDOC > CAST((GETDATE() - 30) AS int) + AND SAMA1.dbo.LOTSER.NUMLOT <> '00000000000' + GROUP BY + SAMA1.dbo.LOTSER.NUMLOT, + SAMA1.dbo.ARTICO.CODICE, + SAMA1.dbo.FATRIG.DESCR, + LEFT(SAMA1.dbo.LOTSER.NUMSER, 6), + SAMA1.dbo.BAMTES.NUMDOC, + SAMA1.dbo.BAMTES.DATDOC, + SAMA1.dbo.BAMTES.ID, + SAMA1.dbo.BAMTES.DESCRDEST, + SAMA1.dbo.NAZIONI.CODICE, + SAMA1.dbo.NAZIONI.DESCR, + SAMA1.dbo.BAMTES.IDMTRASP, + SAMA1.dbo.MTRASP.CODICE +), +Base AS ( + SELECT + prep.UDC AS Pallet, + prep.NUMLOT AS Lotto, + prep.CODICE AS Articolo, + prep.DESCR AS Descrizione, + prep.Qta, + prep.NUMDOC AS Documento, + prep.DATDOC AS DataDocumento, + prep.Expr1 AS CodNazione, + prep.NAZIONE, + CASE + WHEN prep.Expr1 = 'DE' THEN 10 + WHEN prep.Expr1 = 'TH' THEN CASE WHEN SUBSTRING(prep.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END + WHEN prep.Expr1 = 'MEX' THEN 12 + ELSE 4 + END AS Stato, + ISNULL(g.NumeroPallet, 0) AS PalletCella, + ISNULL(g.IDMagazzino, 1) AS Magazzino, + ISNULL(g.IDArea, 5) AS Area, + ISNULL(g.IDCella, 1000) AS Cella, + ISNULL(c.Ordinamento, 99999) AS Ordinamento, + ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione, + SUBSTRING(prep.DESCRDEST, 1, 2) AS DEST + FROM dbo.Celle AS c + INNER JOIN dbo.XMag_GiacenzaPallet AS g + ON c.ID = g.IDCella + RIGHT OUTER JOIN PreparaPackingList AS prep + ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC + WHERE prep.NUMLOT <> '' + GROUP BY + prep.Expr1, + prep.NAZIONE, + prep.UDC, + prep.NUMDOC, + prep.DATDOC, + prep.NUMLOT, + prep.CODICE, + prep.DESCR, + prep.Qta, + g.NumeroPallet, + g.IDMagazzino, + g.IDArea, + g.IDCella, + c.Ordinamento, + c.Corsia, + c.Colonna, + c.Fila, + prep.DESCRDEST +) +SELECT TOP 10000 + Base.Pallet, + Base.Lotto, + Base.Articolo, + Base.Descrizione, + Base.Qta, + Base.Documento, + Base.DataDocumento, + Base.CodNazione, + Base.NAZIONE, + Base.Stato, + Base.PalletCella, + Base.Magazzino, + Base.Area, + Base.Cella, + Base.Ordinamento, + Base.Ubicazione, + Base.DEST, + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(Base.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END AS IDStato +FROM Base +LEFT JOIN dbo.PyPickingListReservation AS pr + ON pr.ID = 1 + AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL +ORDER BY + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(Base.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END DESC, + Base.Documento, + Base.Ordinamento; +GO + +CREATE OR ALTER VIEW [dbo].[py_ViewPackingListRestante] +AS +SELECT + Pallet, + Lotto, + Articolo, + Descrizione, + Qta, + Documento, + DataDocumento, + CodNazione, + NAZIONE, + Stato, + PalletCella, + Magazzino, + Area, + Cella, + Ordinamento, + Ubicazione, + DEST, + IDStato +FROM dbo.py_XMag_ViewPackingList +WHERE Cella <> 9999; +GO diff --git a/apply_pickinglist_optional_destination_country_patch.sql b/apply_pickinglist_optional_destination_country_patch.sql new file mode 100644 index 0000000..e8e1997 --- /dev/null +++ b/apply_pickinglist_optional_destination_country_patch.sql @@ -0,0 +1,217 @@ +/* +Patch Python WMS: picking list con nazione di destinazione non valorizzata + +Problema risolto: +- una testata BAMTES valida puo' avere IDNAZDEST NULL, in particolare quando + viene scelto un indirizzo di destinazione alternativo privo di nazione; +- l'INNER JOIN a SAMA1.dbo.NAZIONI eliminava l'intero documento; +- la picking list non compariva quindi in Gestione Picking List ne' nello + storico Python, anche se stato, data, righe, lotti e UDC erano validi. + +Strategia: +- il collegamento BAMTES -> NAZIONI diventa LEFT OUTER JOIN; +- non viene inventato alcun codice nazione: Expr1 e NAZIONE restano NULL; +- il CASE gia' esistente assegna in questo caso lo Stato generico 4; +- nessun oggetto legacy usato dal programma C# viene modificato. + +Oggetti Python modificati: +- dbo.py_vPreparaPackingListSAMA1 (storico e base condivisa); +- dbo.py_XMag_ViewPackingList (gestione operativa). +*/ + +SET XACT_ABORT ON; +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER VIEW dbo.py_vPreparaPackingListSAMA1 +AS +SELECT + ls.NUMLOT, + a.CODICE, + fr.DESCR, + LEFT(ls.NUMSER, 6) AS UDC, + SUM(ls.QTTGIAI) AS Qta, + bt.NUMDOC, + bt.DATDOC, + bt.ID, + bt.DESCRDEST, + bt.STATO AS StatoDocumento, + n.CODICE AS Expr1, + mt.CODICE + ' ' + n.DESCR AS NAZIONE, + bt.IDMTRASP +FROM SAMA1.dbo.BAMTES AS bt +INNER JOIN SAMA1.dbo.FATRIG AS fr + ON fr.IDBAM = bt.ID +INNER JOIN SAMA1.dbo.LOTTIBF AS lb + ON lb.IDFATRIG = fr.ID +INNER JOIN SAMA1.dbo.LOTSER AS ls + ON ls.ID = lb.IDLOTSER +LEFT OUTER JOIN SAMA1.dbo.ARTICO AS a + ON a.ID = ls.IDARTICO +LEFT OUTER JOIN SAMA1.sam.EXTUC AS uc + ON uc.CODICE = LEFT(ls.NUMSER, 6) +LEFT OUTER JOIN SAMA1.dbo.MTRASP AS mt + ON mt.ID = bt.IDMTRASP +LEFT OUTER JOIN SAMA1.dbo.NAZIONI AS n + ON n.ID = bt.IDNAZDEST +WHERE bt.ANNDOC >= YEAR(GETDATE()) + AND bt.STATO IN ('P', 'D') + AND ls.NUMLOT <> '00000000000' +GROUP BY + ls.NUMLOT, + a.CODICE, + fr.DESCR, + LEFT(ls.NUMSER, 6), + bt.NUMDOC, + bt.DATDOC, + bt.ID, + bt.DESCRDEST, + bt.STATO, + n.CODICE, + n.DESCR, + bt.IDMTRASP, + mt.CODICE; +GO + +CREATE OR ALTER VIEW dbo.py_XMag_ViewPackingList +AS +WITH PreparaPackingList AS ( + SELECT + ls.NUMLOT, + a.CODICE, + fr.DESCR, + LEFT(ls.NUMSER, 6) AS UDC, + SUM(ls.QTTGIAI) AS Qta, + bt.NUMDOC, + bt.DATDOC, + bt.ID, + bt.DESCRDEST, + n.CODICE AS Expr1, + mt.CODICE + ' ' + n.DESCR AS NAZIONE, + bt.IDMTRASP + FROM SAMA1.dbo.BAMTES AS bt + INNER JOIN SAMA1.dbo.FATRIG AS fr + ON fr.IDBAM = bt.ID + INNER JOIN SAMA1.dbo.LOTTIBF AS lb + ON lb.IDFATRIG = fr.ID + INNER JOIN SAMA1.dbo.LOTSER AS ls + ON ls.ID = lb.IDLOTSER + LEFT OUTER JOIN SAMA1.dbo.ARTICO AS a + ON a.ID = ls.IDARTICO + LEFT OUTER JOIN SAMA1.sam.EXTUC AS uc + ON uc.CODICE = LEFT(ls.NUMSER, 6) + LEFT OUTER JOIN SAMA1.dbo.MTRASP AS mt + ON mt.ID = bt.IDMTRASP + LEFT OUTER JOIN SAMA1.dbo.NAZIONI AS n + ON n.ID = bt.IDNAZDEST + WHERE bt.ANNDOC >= YEAR(GETDATE()) + AND bt.STATO = 'P' + AND bt.DATDOC > CAST((GETDATE() - 30) AS int) + AND ls.NUMLOT <> '00000000000' + GROUP BY + ls.NUMLOT, + a.CODICE, + fr.DESCR, + LEFT(ls.NUMSER, 6), + bt.NUMDOC, + bt.DATDOC, + bt.ID, + bt.DESCRDEST, + n.CODICE, + n.DESCR, + bt.IDMTRASP, + mt.CODICE +), +Base AS ( + SELECT + prep.UDC AS Pallet, + prep.NUMLOT AS Lotto, + prep.CODICE AS Articolo, + prep.DESCR AS Descrizione, + prep.Qta, + prep.NUMDOC AS Documento, + prep.DATDOC AS DataDocumento, + prep.Expr1 AS CodNazione, + prep.NAZIONE, + CASE + WHEN prep.Expr1 = 'DE' THEN 10 + WHEN prep.Expr1 = 'TH' THEN + CASE WHEN SUBSTRING(prep.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END + WHEN prep.Expr1 = 'MEX' THEN 12 + ELSE 4 + END AS Stato, + ISNULL(g.NumeroPallet, 0) AS PalletCella, + ISNULL(g.IDMagazzino, 1) AS Magazzino, + ISNULL(g.IDArea, 5) AS Area, + ISNULL(g.IDCella, 1000) AS Cella, + ISNULL(c.Ordinamento, 99999) AS Ordinamento, + ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione, + SUBSTRING(prep.DESCRDEST, 1, 2) AS DEST + FROM dbo.Celle AS c + INNER JOIN dbo.XMag_GiacenzaPallet AS g + ON c.ID = g.IDCella + RIGHT OUTER JOIN PreparaPackingList AS prep + ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC + WHERE prep.NUMLOT <> '' + GROUP BY + prep.Expr1, + prep.NAZIONE, + prep.UDC, + prep.NUMDOC, + prep.DATDOC, + prep.NUMLOT, + prep.CODICE, + prep.DESCR, + prep.Qta, + g.NumeroPallet, + g.IDMagazzino, + g.IDArea, + g.IDCella, + c.Ordinamento, + c.Corsia, + c.Colonna, + c.Fila, + prep.DESCRDEST +) +SELECT TOP (10000) + Base.Pallet, + Base.Lotto, + Base.Articolo, + Base.Descrizione, + Base.Qta, + Base.Documento, + Base.DataDocumento, + Base.CodNazione, + Base.NAZIONE, + Base.Stato, + Base.PalletCella, + Base.Magazzino, + Base.Area, + Base.Cella, + Base.Ordinamento, + Base.Ubicazione, + Base.DEST, + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(Base.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END AS IDStato +FROM Base +LEFT JOIN dbo.PyPickingListReservation AS pr + ON pr.ID = 1 + AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL +ORDER BY + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(Base.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END DESC, + Base.Documento, + Base.Ordinamento; +GO + +PRINT 'Patch nazione destinazione opzionale applicata alle viste Python.'; +GO diff --git a/apply_pickinglist_sequence_patch.sql b/apply_pickinglist_sequence_patch.sql new file mode 100644 index 0000000..f73258d --- /dev/null +++ b/apply_pickinglist_sequence_patch.sql @@ -0,0 +1,136 @@ +/* +Patch Python WMS: ordine di prelievo per singola picking list + +Prerequisito: +- eseguire prima apply_pickinglist_days_filter_patch.sql, che espone + DataDocumento nelle viste operative Python. + +Effetti: +- crea una tabella Python-only con la sequenza personalizzata per Documento; +- crea una vista Python-only con l'ordine effettivo condiviso da backoffice e barcode; +- senza configurazione applica il default: Non scaff. prima degli scaffali; +- con configurazione applica i gruppi scelti e accoda in modo sicuro quelli mancanti; +- non modifica tabelle, viste o stored procedure legacy usate dal programma C#. +*/ + +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +IF OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') IS NULL +BEGIN + CREATE TABLE dbo.py_PickingListPrelievoSequenza ( + ID bigint IDENTITY(1,1) NOT NULL + CONSTRAINT PK_py_PickingListPrelievoSequenza PRIMARY KEY, + Documento varchar(50) NOT NULL, + GruppoChiave varchar(160) NOT NULL, + GruppoEtichetta varchar(100) NOT NULL, + Ordine int NOT NULL, + InsUtente varchar(50) NULL, + InsDataOra datetime2(0) NOT NULL + CONSTRAINT DF_py_PickingListPrelievoSequenza_InsDataOra DEFAULT SYSDATETIME(), + ModUtente varchar(50) NULL, + ModDataOra datetime2(0) NULL, + DelUtente varchar(50) NULL, + DelDataOra datetime2(0) NULL, + CONSTRAINT CK_py_PickingListPrelievoSequenza_Ordine CHECK (Ordine >= 0) + ); +END +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') + AND name = N'UX_py_PickingListPrelievoSequenza_DocGruppo' +) +BEGIN + CREATE UNIQUE INDEX UX_py_PickingListPrelievoSequenza_DocGruppo + ON dbo.py_PickingListPrelievoSequenza (Documento, GruppoChiave) + WHERE DelDataOra IS NULL; +END +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') + AND name = N'UX_py_PickingListPrelievoSequenza_DocOrdine' +) +BEGIN + CREATE UNIQUE INDEX UX_py_PickingListPrelievoSequenza_DocOrdine + ON dbo.py_PickingListPrelievoSequenza (Documento, Ordine) + WHERE DelDataOra IS NULL; +END +GO + +CREATE OR ALTER VIEW dbo.py_ViewPackingListPrelievo +AS +WITH Gruppi AS ( + SELECT + pl.*, + CASE + WHEN pl.Cella = 1000 THEN 'NON_SCAFF' + ELSE CONCAT( + 'M:', pl.Magazzino, + '|A:', pl.Area, + '|C:', + LTRIM(RTRIM( + CASE + WHEN CHARINDEX(' - ', pl.Ubicazione) > 0 + THEN LEFT(pl.Ubicazione, CHARINDEX(' - ', pl.Ubicazione) - 1) + ELSE pl.Ubicazione + END + )) + ) + END AS GruppoPrelievoChiave, + CASE + WHEN pl.Cella = 1000 THEN 'Non scaff.' + ELSE LTRIM(RTRIM( + CASE + WHEN CHARINDEX(' - ', pl.Ubicazione) > 0 + THEN LEFT(pl.Ubicazione, CHARINDEX(' - ', pl.Ubicazione) - 1) + ELSE pl.Ubicazione + END + )) + END AS GruppoPrelievo, + CASE + WHEN pl.Cella = 1000 THEN 0 + ELSE 1000 + ISNULL(pl.Ordinamento, 99999) + END AS OrdineGruppoDefault + FROM dbo.py_ViewPackingListRestante AS pl +), Sequenza AS ( + SELECT + g.*, + s.Ordine AS OrdineGruppoConfigurato, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.py_PickingListPrelievoSequenza AS any_s + WHERE any_s.Documento COLLATE Latin1_General_CI_AS = + CAST(g.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS + AND any_s.DelDataOra IS NULL + ) THEN 1 + ELSE 0 + END AS HaSequenzaPersonalizzata + FROM Gruppi AS g + LEFT JOIN dbo.py_PickingListPrelievoSequenza AS s + ON s.Documento COLLATE Latin1_General_CI_AS = + CAST(g.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS + AND s.GruppoChiave COLLATE Latin1_General_CI_AS = + g.GruppoPrelievoChiave COLLATE Latin1_General_CI_AS + AND s.DelDataOra IS NULL +) +SELECT + Sequenza.*, + CASE + WHEN OrdineGruppoConfigurato IS NOT NULL THEN OrdineGruppoConfigurato + WHEN HaSequenzaPersonalizzata = 1 THEN 100000 + OrdineGruppoDefault + ELSE OrdineGruppoDefault + END AS OrdinePrelievoGruppo +FROM Sequenza; +GO + +PRINT 'Patch sequenza prelievo Python applicata.'; +GO diff --git a/barcode_repository.py b/barcode_repository.py index 94937d7..0f33b75 100644 --- a/barcode_repository.py +++ b/barcode_repository.py @@ -26,7 +26,7 @@ SELECT TOP (1) Ubicazione, Ordinamento, IDStato -FROM dbo.py_XMag_ViewPackingList AS pl +FROM dbo.py_ViewPackingListPrelievo AS pl WHERE Ordinamento > 0 AND IDStato = :id_stato AND NOT EXISTS ( @@ -39,7 +39,7 @@ WHERE Ordinamento > 0 AND s.IDStato = pl.IDStato AND s.Risolto = 0 ) -ORDER BY Ordinamento; +ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet; """ SQL_PICKING_BY_PALLET = """ @@ -53,9 +53,9 @@ SELECT TOP (1) Ubicazione, Ordinamento, IDStato -FROM dbo.py_XMag_ViewPackingList +FROM dbo.py_ViewPackingListPrelievo WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS -ORDER BY Ordinamento; +ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet; """ SQL_TRACE_BY_PALLET = """ diff --git a/gestione_pickinglist.py b/gestione_pickinglist.py index 56cb6a7..eb00cf7 100644 --- a/gestione_pickinglist.py +++ b/gestione_pickinglist.py @@ -81,6 +81,7 @@ from ui_tables import ( from user_session import UserSession from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later +from pickinglist_sequence import open_pickinglist_sequence_window __version__ = module_version(__name__) @@ -227,18 +228,21 @@ SELECT MAX(Magazzino) AS Magazzino, MAX(Area) AS Area, MAX(Cella) AS Cella, + MIN(OrdinePrelievoGruppo) AS OrdinePrelievoGruppo, MIN(Ordinamento) AS Ordinamento, MAX(IDStato) AS IDStato -FROM dbo.py_ViewPackingListRestante +FROM dbo.py_ViewPackingListPrelievo +WHERE DataDocumento > CAST((GETDATE() - :days) AS int) GROUP BY Documento, CodNazione, NAZIONE, Stato -ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato; +ORDER BY MIN(OrdinePrelievoGruppo), MIN(Ordinamento), Documento, NAZIONE, Stato; """ SQL_PL_DETAILS = """ SELECT * -FROM dbo.py_ViewPackingListRestante +FROM dbo.py_ViewPackingListPrelievo WHERE Documento = :Documento -ORDER BY Ordinamento; + AND DataDocumento > CAST((GETDATE() - :days) AS int) +ORDER BY OrdinePrelievoGruppo, Ordinamento, Pallet; """ # -------------------- helpers -------------------- @@ -649,6 +653,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self.rows_models: list[PLRow] = [] self._detail_cache: Dict[Any, list] = {} self.detail_doc = None + self.days_var = tk.StringVar(value="10") self._detail_sort_key: Optional[str] = None self._detail_sort_reverse = False self._detail_sorting = False @@ -699,6 +704,7 @@ class GestionePickingListFrame(ctk.CTkFrame): (loc_text("picking.button.reload", catalog=self._locale_catalog, default="Ricarica"), self.reload_from_db), (loc_text("picking.button.prenota", catalog=self._locale_catalog, default="Prenota"), self.on_prenota), (loc_text("picking.button.sprenota", catalog=self._locale_catalog, default="S-prenota"), self.on_sprenota), + (loc_text("picking.button.sequence", catalog=self._locale_catalog, default="Sequenza di prelievo"), self.on_sequence), (loc_text("picking.button.export", catalog=self._locale_catalog, default="Esporta XLSX"), self.on_export) ]): ctk.CTkButton( @@ -708,6 +714,20 @@ class GestionePickingListFrame(ctk.CTkFrame): font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), ).grid(row=0, column=i, padx=6) + ctk.CTkLabel( + top, + text=loc_text("picking.label.days", catalog=self._locale_catalog, default="Giorni:"), + font=theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)), + ).grid(row=0, column=5, padx=(18, 4), sticky="e") + ctk.CTkOptionMenu( + top, + values=["10", "15", "20", "30"], + variable=self.days_var, + width=82, + command=lambda _value: self.reload_from_db(), + font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), + ).grid(row=0, column=6, padx=(0, 6), sticky="w") + # --- micro spinner a destra della toolbar --- self.spinner = ToolbarSpinner(top) self.spinner.widget().grid(row=0, column=10, padx=(8,0)) # largo spazio a destra @@ -998,6 +1018,15 @@ class GestionePickingListFrame(ctk.CTkFrame): documento = _s(self.detail_doc) return documento or None + def _selected_days(self) -> int: + """Return the selected look-back interval for operational picking lists.""" + + try: + days = int(str(self.days_var.get()).strip()) + except Exception: + days = 10 + return days if days in (10, 15, 20, 30) else 10 + # ----- eventi ----- @_log_call() def on_row_checked(self, model: PLRow, is_checked: bool): @@ -1013,10 +1042,12 @@ class GestionePickingListFrame(ctk.CTkFrame): self.detail_doc = model.pl.get("Documento") _MODULE_LOGGER.info(f"Documento selezionato per il dettaglio: {self.detail_doc}") self.spinner.start(" Carico dettagli…") # spinner ON + days = self._selected_days() async def _job(): - _log_sql("SQL_PL_DETAILS", SQL_PL_DETAILS, {"Documento": self.detail_doc}) - return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc}) + params = {"Documento": self.detail_doc, "days": days} + _log_sql("SQL_PL_DETAILS", SQL_PL_DETAILS, params) + return await self.db_client.query_json(SQL_PL_DETAILS, params) def _ok(res): # NON fermare lo spinner subito: lo farà _refresh_details_incremental @@ -1058,9 +1089,11 @@ class GestionePickingListFrame(ctk.CTkFrame): # document selected when it is still visible. reselect_documento = self._selected_documento_for_reload() self.spinner.start(" Carico…") # spinner ON + days = self._selected_days() async def _job(): - _log_sql("SQL_PL", SQL_PL, {}) - return await self.db_client.query_json(SQL_PL, {}) + params = {"days": days} + _log_sql("SQL_PL", SQL_PL, params) + return await self.db_client.query_json(SQL_PL, params) def _on_success(res): rows = _rows_to_dicts(res) _log_dataset("SQL_PL", rows) @@ -1156,6 +1189,45 @@ class GestionePickingListFrame(ctk.CTkFrame): self._load_detail_sheet_data(self._detail_rows_to_sheet_data(rows)) # ----- azioni ----- + @_log_call() + def on_sequence(self): + """Open the pickup-sequence editor for the selected picking list.""" + + if not self._can("pickinglist.sequence"): + messagebox.showwarning( + loc_text("picking.sequence.title", catalog=self._locale_catalog, default="Sequenza di prelievo"), + loc_text("picking.sequence.msg.denied", catalog=self._locale_catalog, default="Permesso negato."), + parent=self, + ) + return + + model = self._get_selected_model() + if not model: + messagebox.showinfo( + loc_text("picking.sequence.title", catalog=self._locale_catalog, default="Sequenza di prelievo"), + loc_text( + "picking.sequence.msg.select", + catalog=self._locale_catalog, + default="Seleziona una Picking List prima di definire la sequenza.", + ), + parent=self, + ) + return + + documento = _s(model.pl.get("Documento")) + + def _on_saved(): + self._detail_cache.pop(documento, None) + self.reload_from_db(reselect_documento=documento) + + open_pickinglist_sequence_window( + self.winfo_toplevel(), + self.db_client, + documento=documento, + session=self.session, + on_saved=_on_saved, + ) + @_log_call() def on_prenota(self): """Reserve the selected picking list.""" diff --git a/locale.json b/locale.json index 3198a3f..51ad915 100644 --- a/locale.json +++ b/locale.json @@ -121,7 +121,30 @@ "picking.button.reload": "Ricarica", "picking.button.prenota": "Prenota", "picking.button.sprenota": "S-prenota", + "picking.button.sequence": "Sequenza di prelievo", "picking.button.export": "Esporta XLSX", + "picking.label.days": "Giorni:", + "picking.sequence.title": "Sequenza di prelievo", + "picking.sequence.document": "Picking List {documento}", + "picking.sequence.available": "Scaffali disponibili", + "picking.sequence.selected": "Ordine personalizzato", + "picking.sequence.hint": "Trascina gli scaffali a destra. Quelli non inseriti saranno accodati secondo l'ordine predefinito.", + "picking.sequence.default_hint": "Default: prima Non scaff., poi gli scaffali secondo l'ordine WMS.", + "picking.sequence.button.add": "Aggiungi >", + "picking.sequence.button.remove": "< Rimuovi", + "picking.sequence.button.up": "Su", + "picking.sequence.button.down": "Giu'", + "picking.sequence.button.save": "Salva", + "picking.sequence.button.reset": "Ripristina default", + "picking.sequence.button.close": "Chiudi", + "picking.sequence.busy.load": "Carico gli scaffali...", + "picking.sequence.busy.save": "Salvo la sequenza...", + "picking.sequence.msg.select": "Seleziona una Picking List prima di definire la sequenza.", + "picking.sequence.msg.denied": "Permesso negato.", + "picking.sequence.msg.saved": "Sequenza salvata.", + "picking.sequence.msg.reset": "Sequenza personalizzata rimossa. E' attivo l'ordine predefinito.", + "picking.sequence.msg.load_error": "Errore durante il caricamento:\n{error}", + "picking.sequence.msg.save_error": "Errore durante il salvataggio:\n{error}", "scarico.title": "Scarica {ubicazione}", "scarico.label.location": "Ubicazione: {ubicazione}", "scarico.label.select": "Seleziona le UDC da scaricare", @@ -258,7 +281,30 @@ "picking.button.reload": "Reload", "picking.button.prenota": "Reserve", "picking.button.sprenota": "Unreserve", + "picking.button.sequence": "Pickup sequence", "picking.button.export": "Export XLSX", + "picking.label.days": "Days:", + "picking.sequence.title": "Pickup sequence", + "picking.sequence.document": "Picking List {documento}", + "picking.sequence.available": "Available racks", + "picking.sequence.selected": "Custom order", + "picking.sequence.hint": "Drag racks to the right. Missing racks are appended using the default order.", + "picking.sequence.default_hint": "Default: non-shelved first, then racks in WMS order.", + "picking.sequence.button.add": "Add >", + "picking.sequence.button.remove": "< Remove", + "picking.sequence.button.up": "Up", + "picking.sequence.button.down": "Down", + "picking.sequence.button.save": "Save", + "picking.sequence.button.reset": "Restore default", + "picking.sequence.button.close": "Close", + "picking.sequence.busy.load": "Loading racks...", + "picking.sequence.busy.save": "Saving sequence...", + "picking.sequence.msg.select": "Select a Picking List before defining its sequence.", + "picking.sequence.msg.denied": "Permission denied.", + "picking.sequence.msg.saved": "Sequence saved.", + "picking.sequence.msg.reset": "Custom sequence removed. The default order is active.", + "picking.sequence.msg.load_error": "Load failed:\n{error}", + "picking.sequence.msg.save_error": "Save failed:\n{error}", "scarico.title": "Unload {ubicazione}", "scarico.label.location": "Location: {ubicazione}", "scarico.label.select": "Select the UDCs to unload", diff --git a/ordinamento.md b/ordinamento.md new file mode 100644 index 0000000..2374e8e --- /dev/null +++ b/ordinamento.md @@ -0,0 +1,414 @@ +# Ordinamento di prelievo delle celle + +## 1. Scopo del documento + +Questo documento descrive il significato e l'origine del campo +`dbo.Celle.Ordinamento`, il modo in cui viene utilizzato per proporre le UDC al +barcode e le differenze riscontrate tra i due magazzini storici: + +- Area 5, identificata nel database legacy come magazzino `MDE5`; +- Area 6, identificata nel database legacy come magazzino `MDE6`. + +Nel linguaggio operativo si parla di Area 5 e Area 6. Nel modello legacy, +tuttavia, la tabella `Magazzini` contiene `MDE5` e `MDE6`, mentre la tabella +`Aree` contiene le singole corsie o scaffali. Gli ID della tabella `Aree` non +devono quindi essere confusi con i nomi operativi Area 5 e Area 6. + +Le conclusioni riportate derivano da: + +- struttura e dati correnti della tabella `dbo.Celle`; +- stored procedure legacy contenute in `script.sql`; +- viste usate dalle picking list; +- codice Python del barcode e della gestione picking list; +- confronto tra `ID`, ubicazione e `Ordinamento` delle celle reali. + +## 2. Significato di `Celle.Ordinamento` + +`Celle.Ordinamento` e' una priorita' numerica associata all'anagrafica della +cella. Rappresenta la posizione della cella nel percorso predefinito di +prelievo. + +Il campo non indica: + +- la quantita' di UDC presenti; +- la priorita' della picking list; +- lo stato libero o occupato della cella; +- l'ordine cronologico di inserimento delle UDC; +- l'ID della UDC; +- la distanza calcolata dinamicamente dal barcode. + +Il valore non viene ricalcolato durante un versamento o un prelievo. E' un dato +statico dell'anagrafica della cella, salvo interventi amministrativi o +procedure specifiche di rinumerazione. + +Il tipo SQL storico e' `float`, ma nei dati analizzati viene usato come numero +intero progressivo. Nella futura riprogettazione sarebbe preferibile un tipo +intero o una struttura esplicita di percorso. + +## 3. Dove viene assegnato + +La stored procedure legacy `dbo.spt_SaveCelle` riceve il parametro +`@Ordinamento` e lo salva direttamente: + +```sql +UPDATE Celle +SET Ordinamento = @Ordinamento +WHERE ID = @ID; +``` + +Se la cella non esiste, la stessa stored procedure esegue una `INSERT` e +inserisce il valore ricevuto. La procedura non contiene una formula per +calcolarlo: chi crea o modifica la cella deve decidere il valore e passarlo. + +Nel database non risultano trigger sulla tabella `Celle` che aggiornino +automaticamente `Ordinamento`. + +Sono state individuate anche procedure legacy di supporto: + +- `CreaNuoveCelle`; +- `CreaNuoveCelleMDE6`; +- `CreaNuoveCelleMDE6_BIS`; +- `sp_OrdinaCelle`; +- `spt_SaveCelle`. + +Le procedure di creazione MDE6 impostano normalmente: + +```sql +SET @Ordinamento = @ID; +``` + +La procedura `sp_OrdinaCelle` dimostra invece che il campo puo' essere +rinumerato in un secondo momento per rappresentare un percorso differente +dall'ID della cella. + +## 4. Uso durante una picking list + +Le viste operative associano ogni UDC alla cella corrente e recuperano +`Celle.Ordinamento`. Il barcode richiede la prima riga residua secondo l'ordine +effettivo. + +Prima dell'introduzione della sequenza personalizzata, il criterio principale +era sostanzialmente: + +```sql +ORDER BY Ordinamento; +``` + +Questo significa che il barcode non percorre tutte le celle del magazzino. La +vista contiene soltanto le UDC ancora presenti nella picking list: le celle che +non contengono una UDC richiesta vengono saltate automaticamente. + +Esempio: se il percorso contiene le celle con ordine `1001`, `1002`, `1003` e +`1004`, ma la picking list contiene UDC soltanto nelle celle `1001` e `1004`, il +barcode propone direttamente la UDC di `1001` e poi quella di `1004`. + +## 5. Ordinamento dell'Area 5 / MDE5 + +### 5.1 Caratteristica generale + +Nell'Area 5 `Ordinamento` non coincide normalmente con l'ID della cella. Dai +dati emerge un percorso fisico costruito intenzionalmente, verosimilmente +hardcoded o rinumerato durante la configurazione iniziale del magazzino. + +Le corsie principali coinvolte sono: + +- `1A`; +- `2B`; +- `3C`; +- `4D`. + +Il percorso iniziale delle corsie `1A` e `2B` e' a serpentina. Le due corsie +vengono trattate come lati o scaffali adiacenti di uno stesso tragitto fisico. + +### 5.2 Esempio reale dell'inizio del percorso + +```text +Ordine Ubicazione ID cella +1001 1A.1.e 1005 +1002 1A.1.d 1004 +1003 1A.1.c 1003 +1004 1A.1.b 1002 +1005 1A.1.a 1001 + +1007 2B.1.e 2005 +1008 2B.1.d 2004 +1009 2B.1.c 2003 +1010 2B.1.b 2002 +1011 2B.1.a 2001 + +1013 2B.2.e 2011 +1014 2B.2.d 2010 +1015 2B.2.c 2009 +1016 2B.2.b 2008 +1017 2B.2.a 2007 + +1019 1A.2.e 1011 +1020 1A.2.d 1010 +1021 1A.2.c 1009 +1022 1A.2.b 1008 +1023 1A.2.a 1007 +``` + +La sequenza prosegue con lo stesso principio: + +```text +1A campata 1 +2B campata 1 +2B campata 2 +1A campata 2 +1A campata 3 +2B campata 3 +2B campata 4 +1A campata 4 +... +``` + +### 5.3 Movimento verticale nella campata + +All'interno di ciascuna campata l'ordine verificato e': + +```text +e -> d -> c -> b -> a +``` + +Il percorso procede quindi dal livello identificato con `e` verso il livello +`a`. Non viene prelevata una sola UDC per campata per poi ricominciare il giro. +Vengono considerate, in sequenza, tutte le celle richieste della campata +corrente prima di passare alla campata successiva. + +Se una campata contiene piu' UDC richieste in livelli diversi, il barcode le +propone secondo `e, d, c, b, a`. Se alcuni livelli non contengono UDC della +picking list, vengono semplicemente saltati. + +### 5.4 Alternanza tra 1A e 2B + +Il sistema non esaurisce tutto `1A` prima di iniziare `2B`. Alterna blocchi di +campate tra le due corsie per seguire un tragitto a serpentina ed evitare +ritorni inutili. + +Il comportamento predefinito storico e' quindi: + +- completare le UDC richieste nella campata corrente; +- passare alla campata fisicamente successiva del percorso; +- alternare `1A` e `2B` secondo la serpentina configurata; +- non tornare all'inizio dopo ogni singola UDC. + +### 5.5 Passaggio a 3C e 4D + +Le corsie `3C` e `4D` hanno valori di ordinamento molto piu' alti. Nei dati +analizzati: + +```text +1A: minimo 1001, massimo 1275 +2B: minimo 1007, massimo 1251 +3C: minimo 10019, massimo 10227 +4D: valori nell'intervallo 10055 - 10233 circa +``` + +Di conseguenza il percorso storico esaurisce normalmente il blocco `1A/2B` +prima di passare al blocco `3C/4D`. + +La presenza di famiglie numeriche differenti indica che l'ordinamento +dell'Area 5 e' stato costruito o modificato in piu' fasi storiche. + +### 5.6 Significato dei numeri mancanti + +La progressione contiene intervalli non utilizzati, per esempio `1005` seguito +da `1007`. Il numero mancante non indica necessariamente una cella eliminata o +un errore. Puo' essere stato lasciato come separatore tra campate o derivare +dalla procedura di rinumerazione. + +Il software deve usare il confronto numerico e non deve presumere che tutti i +valori siano consecutivi. + +## 6. Ordinamento dell'Area 6 / MDE6 + +### 6.1 Caratteristica generale + +Nell'Area 6 il modello e' piu' semplice. La maggior parte delle celle e' stata +generata con `Ordinamento` uguale all'ID della cella. + +Le famiglie principali sono: + +```text +1M -> celle e ordinamenti 5001, 5002, 5003, ... +2N -> celle e ordinamenti 6001, 6002, 6003, ... +3O -> celle e ordinamenti 7001, 7002, 7003, ... +4P -> celle e ordinamenti 8001, 8002, 8003, ... +``` + +Le procedure `CreaNuoveCelleMDE6` e `CreaNuoveCelleMDE6_BIS` creano le celle +con cicli annidati e assegnano esplicitamente: + +```sql +SET @Ordinamento = @ID; +``` + +### 6.2 Ordine degli scaffali + +Poiche' le famiglie numeriche sono separate, l'ordine globale predefinito e': + +```text +1M -> 2N -> 3O -> 4P +``` + +Senza una sequenza personalizzata vengono quindi considerate prima tutte le +UDC richieste in `1M`, poi quelle in `2N`, successivamente `3O` e infine `4P`. + +### 6.3 Ordine interno dello scaffale + +Durante la generazione delle celle, i cicli legacy incrementano prima la +campata e, all'interno della campata, il piano. L'ordine risultante e': + +```text +1M.1.a -> 1M.1.b -> 1M.1.c -> 1M.1.d -> 1M.1.e +1M.2.a -> 1M.2.b -> 1M.2.c -> 1M.2.d -> 1M.2.e +... +2N.1.a -> 2N.1.b -> ... +``` + +Quindi l'Area 6 differisce dall'Area 5 in due aspetti principali: + +- non presenta l'alternanza a serpentina tra due corsie adiacenti; +- dentro la campata procede da `a` verso `e`, mentre nell'Area 5 verificata + procede da `e` verso `a`. + +### 6.4 Limiti della ricostruzione + +Il comportamento descritto deriva dalla procedura di generazione e dai valori +correnti. Eventuali celle aggiunte manualmente in tempi successivi possono +avere eccezioni. L'applicazione deve sempre leggere il valore effettivo dal +database e non ricostruirlo aritmeticamente dall'ubicazione. + +## 7. Celle virtuali + +Nel sistema legacy esistono due ubicazioni convenzionali: + +```text +ID 1000 -> 5E.1.1 -> Non scaff. -> Ordinamento 99999 +ID 9999 -> 7G.1.1 -> Spedita -> Ordinamento 0 +``` + +Questi valori non rappresentano un percorso fisico: + +- `5E.1.1` identifica UDC presenti nel magazzino ma non allocate in uno + scaffale fisico; +- `7G.1.1` identifica UDC spedite. + +Storicamente `Non scaff.` aveva ordinamento `99999`, quindi finiva in fondo +alla picking list. Il nuovo requisito operativo stabilisce invece che le UDC +non scaffalate devono essere proposte per prime. + +La nuova vista Python non modifica `Celle.Ordinamento`: assegna a `Non scaff.` +una precedenza logica di gruppo pari a zero. La cella `Spedita` non entra nella +vista residuale e quindi non viene proposta dal barcode. + +## 8. Nuovo ordinamento predefinito Python + +In assenza di una configurazione specifica per il documento, il nuovo ordine +effettivo e': + +1. tutte le UDC `Non scaff.` ancora appartenenti alla picking list; +2. tutte le UDC nelle celle fisiche secondo `Celle.Ordinamento`; +3. codice pallet come criterio stabile in caso di parita'. + +In forma semplificata: + +```sql +ORDER BY + OrdinePrelievoGruppo, + Ordinamento, + Pallet; +``` + +Per le celle fisiche, se non esiste una personalizzazione, +`OrdinePrelievoGruppo` conserva la progressione storica. Il percorso dell'Area +5 rimane quindi a serpentina e quello dell'Area 6 rimane progressivo. + +## 9. Effetto della sequenza personalizzata + +La sequenza personalizzata opera a livello di gruppo o scaffale, non a livello +di singola cella. + +Esempio di configurazione: + +```text +1. Non scaff. +2. 3C +3. 1A +4. 2B +5. 4D +``` + +In questo caso il sistema esaurisce prima il gruppo `Non scaff.`, poi tutte le +UDC di `3C`, quindi tutte quelle di `1A` e cosi' via. + +All'interno di ciascun gruppo continua a usare `Celle.Ordinamento`. Per +esempio, se `1A` viene posizionata prima di `2B`, non viene piu' conservata +l'alternanza storica tra `1A` e `2B`: vengono esaurite prima tutte le UDC di +`1A`, nel loro ordine interno, e successivamente quelle di `2B`. + +Questa differenza e' intenzionale: + +- ordine predefinito: conserva il percorso storico globale; +- ordine personalizzato: impone la precedenza degli scaffali scelta + dall'operatore e conserva l'ordine storico soltanto dentro ogni scaffale. + +Se la sequenza personalizzata non include tutti i gruppi, quelli mancanti +vengono accodati secondo il percorso predefinito. Nessuna UDC viene esclusa. + +## 10. UDC multiple, lotti e parita' di ordine + +Piu' righe della vista possono riferirsi alla stessa UDC quando contiene piu' +lotti o articoli. Questo non significa che il magazziniere debba movimentare +fisicamente la stessa UDC piu' volte: l'unita' di movimentazione rimane il +pallet/UDC. + +Piu' UDC possono inoltre risultare associate alla stessa cella nel modello +legacy. In quel caso condividono lo stesso `Ordinamento`. Il nuovo codice usa +anche `Pallet` come criterio finale per rendere stabile la sequenza, ma il +percorso fisico resta quello della cella. + +## 11. Considerazioni per la futura riprogettazione + +Nel futuro FlyWMS Core e' consigliabile separare esplicitamente: + +- ordine fisico della cella dentro lo scaffale; +- ordine predefinito degli scaffali; +- percorso o strategia di visita del magazzino; +- ordine personalizzato per documento; +- priorita' operativa della picking list; +- eventuale ottimizzazione dinamica del percorso. + +Il campo legacy `Celle.Ordinamento` incorpora piu' significati e dipende da +decisioni storiche non completamente documentate. Non dovrebbe essere copiato +alla lettera nel nuovo modello dati senza scomporne la semantica. + +Una possibile struttura futura potrebbe prevedere: + +- `Location.SequenceWithinRack` per l'ordine interno; +- `Rack.DefaultSequence` per l'ordine tra scaffali; +- `Route` e `RouteStep` per percorsi configurabili; +- `PickingDocumentRoute` per la personalizzazione della singola lista. + +## 12. Riferimenti tecnici nel progetto + +- `script.sql`: definizioni di `spt_SaveCelle`, `CreaNuoveCelleMDE6`, + `CreaNuoveCelleMDE6_BIS` e `sp_OrdinaCelle`; +- `barcode_repository.py`: interrogazione della prossima UDC; +- `gestione_pickinglist.py`: visualizzazione del dettaglio ordinato; +- `pickinglist_sequence.py`: gestione della sequenza per documento; +- `apply_pickinglist_sequence_patch.sql`: tabella e vista Python dedicate; +- `specifica_sequenza_prelievo.md`: specifica funzionale della nuova + configurazione. + +## 13. Conclusione + +L'Area 5 utilizza un percorso storico a serpentina, costruito per alternare +corsie e campate secondo la disposizione fisica. L'Area 6 utilizza invece una +progressione piu' semplice, sostanzialmente coincidente con gli ID creati in +sequenza. + +Il nuovo software conserva entrambi i comportamenti come default, anticipa le +UDC non scaffalate e permette di sostituire l'ordine globale degli scaffali per +una singola picking list senza modificare l'anagrafica delle celle. diff --git a/pickinglist_sequence.py b/pickinglist_sequence.py new file mode 100644 index 0000000..75a71bb --- /dev/null +++ b/pickinglist_sequence.py @@ -0,0 +1,584 @@ +"""Editor for the per-document picking-list pickup sequence. + +The barcode client remains unaware of the editor: it reads the effective order +from ``dbo.py_ViewPackingListPrelievo``. This module only manages the Python- +specific configuration rows used by that view. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable +import tkinter as tk +from tkinter import messagebox + +import customtkinter as ctk + +from busy_overlay import InlineBusyOverlay +from gestione_aree import AsyncRunner +from locale_text import load_locale_catalog, text as loc_text +from ui_theme import theme_color, theme_font, theme_section +from user_session import UserSession +from version_info import module_version, versioned_title +from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text + +__version__ = module_version(__name__) + + +SQL_LOAD_GROUPS = """ +SELECT + GruppoPrelievoChiave, + MAX(GruppoPrelievo) AS GruppoPrelievo, + MIN(OrdineGruppoDefault) AS OrdineGruppoDefault, + MIN(OrdineGruppoConfigurato) AS OrdineGruppoConfigurato +FROM dbo.py_ViewPackingListPrelievo +WHERE CAST(Documento AS varchar(50)) = :documento + AND Cella <> 9999 +GROUP BY GruppoPrelievoChiave +ORDER BY + CASE WHEN MIN(OrdineGruppoConfigurato) IS NULL THEN 1 ELSE 0 END, + MIN(OrdineGruppoConfigurato), + MIN(OrdineGruppoDefault), + MAX(GruppoPrelievo); +""" + + +SQL_RESET_SEQUENCE = """ +SET NOCOUNT ON; +SET XACT_ABORT ON; +UPDATE dbo.py_PickingListPrelievoSequenza +SET DelUtente = :utente, + DelDataOra = SYSDATETIME(), + ModUtente = :utente, + ModDataOra = SYSDATETIME() +WHERE Documento = :documento + AND DelDataOra IS NULL; +SELECT CAST(0 AS int) AS ElementiSalvati; +""" + + +def _rows_to_dicts(result: dict[str, Any] | None) -> list[dict[str, Any]]: + """Convert the shared database payload to dictionaries.""" + + if not isinstance(result, dict): + return [] + rows = result.get("rows") or [] + columns = result.get("columns") or [] + if rows and isinstance(rows[0], dict): + return [dict(row) for row in rows] + return [ + {str(columns[index]): row[index] for index in range(min(len(columns), len(row)))} + for row in rows + if isinstance(row, (list, tuple)) + ] + + +@dataclass(frozen=True) +class PickupGroup: + """One rack group, or the conventional non-shelved group.""" + + key: str + label: str + default_order: int + + +class PickingListSequenceWindow(ctk.CTkToplevel): + """Drag-and-drop editor for one picking-list pickup sequence.""" + + def __init__( + self, + parent: tk.Misc, + db_client, + *, + documento: str, + session: UserSession | None, + on_saved: Callable[[], None] | None = None, + ): + super().__init__(parent) + self.db_client = db_client + self.documento = str(documento or "").strip() + self.session = session + self.on_saved = on_saved + self._catalog = load_locale_catalog() + self._tooltip_catalog = load_tooltip_catalog() + self._theme = theme_section("picking_sequence_window", {}) + self.runner = AsyncRunner(self) + self.busy = InlineBusyOverlay(self, self._theme) + self.available: list[PickupGroup] = [] + self.selected: list[PickupGroup] = [] + self._selected_key: str | None = None + self._selected_side: str | None = None + self._drag_group: PickupGroup | None = None + self._drag_side: str | None = None + + self.title(versioned_title(self._t("picking.sequence.title", "Sequenza di prelievo"), __name__)) + self.geometry("780x560") + self.minsize(680, 480) + self.transient(parent) + self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f"))) + self.protocol("WM_DELETE_WINDOW", self.destroy) + self.bind("", lambda _event: self.destroy()) + + self._build_layout() + self.withdraw() + self.after_idle(self._show_and_load) + + def _t(self, key: str, default: str, **values: Any) -> str: + """Return one localized and formatted label.""" + + value = loc_text(key, catalog=self._catalog, default=default) + try: + return value.format(**values) + except Exception: + return value + + def _build_layout(self) -> None: + """Build the two lists and the explicit keyboard-friendly controls.""" + + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(2, weight=1) + self.grid_rowconfigure(2, weight=1) + + ctk.CTkLabel( + self, + text=self._t("picking.sequence.document", "Picking List {documento}", documento=self.documento), + font=theme_font(self._theme, "title_font", ("Segoe UI", 16, "bold")), + ).grid(row=0, column=0, columnspan=3, padx=16, pady=(14, 4), sticky="w") + + ctk.CTkLabel( + self, + text=self._t( + "picking.sequence.hint", + "Trascina gli scaffali a destra. Quelli non inseriti saranno accodati secondo l'ordine predefinito.", + ), + anchor="w", + justify="left", + wraplength=720, + ).grid(row=1, column=0, columnspan=3, padx=16, pady=(0, 10), sticky="ew") + + self.available_panel, self.available_list = self._build_list_panel( + 0, self._t("picking.sequence.available", "Scaffali disponibili") + ) + controls = ctk.CTkFrame(self, fg_color="transparent") + controls.grid(row=2, column=1, padx=10, pady=4) + for label, command in ( + (self._t("picking.sequence.button.add", "Aggiungi >"), self._add_selected), + (self._t("picking.sequence.button.remove", "< Rimuovi"), self._remove_selected), + (self._t("picking.sequence.button.up", "Su"), lambda: self._move_selected(-1)), + (self._t("picking.sequence.button.down", "Giu'"), lambda: self._move_selected(1)), + ): + button = ctk.CTkButton(controls, text=label, width=120, command=command) + button.pack(pady=5) + tooltip_key = { + self._t("picking.sequence.button.add", "Aggiungi >"): "picking.sequence.add", + self._t("picking.sequence.button.remove", "< Rimuovi"): "picking.sequence.remove", + self._t("picking.sequence.button.up", "Su"): "picking.sequence.up", + self._t("picking.sequence.button.down", "Giu'"): "picking.sequence.down", + }.get(label, "") + self._attach_tooltip(button, tooltip_key) + + self.selected_panel, self.selected_list = self._build_list_panel( + 2, self._t("picking.sequence.selected", "Ordine personalizzato") + ) + + ctk.CTkLabel( + self, + text=self._t( + "picking.sequence.default_hint", + "Default: prima Non scaff., poi gli scaffali secondo l'ordine WMS.", + ), + anchor="w", + text_color=theme_color(self._theme, "secondary_text_color", "#4b5563"), + ).grid(row=3, column=0, columnspan=3, padx=16, pady=(8, 4), sticky="ew") + + bottom = ctk.CTkFrame(self) + bottom.grid(row=4, column=0, columnspan=3, padx=16, pady=(6, 14), sticky="ew") + bottom.grid_columnconfigure(0, weight=1) + reset_button = ctk.CTkButton( + bottom, + text=self._t("picking.sequence.button.reset", "Ripristina default"), + command=self._reset_default, + fg_color=theme_color(self._theme, "reset_button_fg_color", "#6b7280"), + ) + reset_button.grid(row=0, column=1, padx=5, pady=8) + self._attach_tooltip(reset_button, "picking.sequence.reset") + save_button = ctk.CTkButton( + bottom, + text=self._t("picking.sequence.button.save", "Salva"), + command=self._save, + ) + save_button.grid(row=0, column=2, padx=5, pady=8) + self._attach_tooltip(save_button, "picking.sequence.save") + close_button = ctk.CTkButton( + bottom, + text=self._t("picking.sequence.button.close", "Chiudi"), + command=self.destroy, + ) + close_button.grid(row=0, column=3, padx=5, pady=8) + self._attach_tooltip(close_button, "picking.sequence.close") + + def _attach_tooltip(self, widget: tk.Misc, key: str) -> None: + """Attach a localized tooltip when the catalog contains one.""" + + value = tooltip_text(key, catalog=self._tooltip_catalog) if key else "" + if value: + WidgetToolTip(widget, value) + + def _build_list_panel(self, column: int, title: str): + """Create one titled, scrollable group list.""" + + panel = ctk.CTkFrame(self) + panel.grid(row=2, column=column, padx=(16 if column == 0 else 0, 16 if column == 2 else 0), pady=4, sticky="nsew") + panel.grid_columnconfigure(0, weight=1) + panel.grid_rowconfigure(1, weight=1) + ctk.CTkLabel(panel, text=title, font=("Segoe UI", 12, "bold")).grid( + row=0, column=0, padx=10, pady=(10, 6), sticky="w" + ) + content = ctk.CTkScrollableFrame(panel, fg_color=theme_color(self._theme, "list_fg_color", "#f8fafc")) + content.grid(row=1, column=0, padx=8, pady=(0, 8), sticky="nsew") + content.grid_columnconfigure(0, weight=1) + return panel, content + + def _show_and_load(self) -> None: + """Center the ready window and start its asynchronous load.""" + + try: + self.update_idletasks() + parent = self.master + x = max(0, parent.winfo_rootx() + (parent.winfo_width() - self.winfo_width()) // 2) + y = max(0, parent.winfo_rooty() + (parent.winfo_height() - self.winfo_height()) // 2) + self.geometry(f"+{x}+{y}") + except Exception: + pass + self.deiconify() + self.lift() + self.focus_force() + self._load() + + def _load(self) -> None: + """Load available groups and the optional saved sequence.""" + + async def _job(): + return await self.db_client.query_json(SQL_LOAD_GROUPS, {"documento": self.documento}) + + def _ok(result): + rows = _rows_to_dicts(result) + configured: list[tuple[int, PickupGroup]] = [] + available: list[PickupGroup] = [] + for row in rows: + group = PickupGroup( + key=str(row.get("GruppoPrelievoChiave") or "").strip(), + label=str(row.get("GruppoPrelievo") or "").strip(), + default_order=int(row.get("OrdineGruppoDefault") or 0), + ) + configured_order = row.get("OrdineGruppoConfigurato") + if configured_order is None: + available.append(group) + else: + configured.append((int(configured_order), group)) + self.available = sorted(available, key=lambda item: (item.default_order, item.label)) + self.selected = [item for _order, item in sorted(configured, key=lambda pair: pair[0])] + self._render_lists() + + def _error(exc): + messagebox.showerror( + self._t("picking.sequence.title", "Sequenza di prelievo"), + self._t("picking.sequence.msg.load_error", "Errore durante il caricamento:\n{error}", error=exc), + parent=self, + ) + + self.runner.run( + _job(), + on_success=_ok, + on_error=_error, + busy=self.busy, + message=self._t("picking.sequence.busy.load", "Carico gli scaffali..."), + ) + + def _render_lists(self) -> None: + """Redraw list buttons after a move or a database load.""" + + for host in (self.available_list, self.selected_list): + for child in host.winfo_children(): + child.destroy() + for row, group in enumerate(self.available): + self._render_group_button(self.available_list, group, "available", row) + for row, group in enumerate(self.selected): + self._render_group_button(self.selected_list, group, "selected", row) + + def _render_group_button(self, host, group: PickupGroup, side: str, row: int) -> None: + """Render one selectable and draggable group button.""" + + is_selected = self._selected_key == group.key and self._selected_side == side + prefix = f"{row + 1}. " if side == "selected" else "" + button = ctk.CTkButton( + host, + text=f"{prefix}{group.label}", + anchor="w", + height=34, + fg_color="#1976d2" if is_selected else theme_color(self._theme, "item_fg_color", "#2ebf74"), + command=lambda g=group, s=side: self._select(g, s), + ) + button.grid(row=row, column=0, padx=4, pady=4, sticky="ew") + button.bind("", lambda _event, g=group, s=side: self._toggle_group(g, s), add="+") + drag_widgets = [button] + for attr in ("_canvas", "_text_label", "_image_label"): + widget = getattr(button, attr, None) + if widget is not None: + drag_widgets.append(widget) + for widget in drag_widgets: + widget.bind("", lambda _event, g=group, s=side: self._start_drag(g, s), add="+") + widget.bind("", self._finish_drag, add="+") + self._attach_tooltip(button, "picking.sequence.item") + + def _select(self, group: PickupGroup, side: str) -> None: + """Select one group for the explicit move controls.""" + + self._selected_key = group.key + self._selected_side = side + self._render_lists() + + def _toggle_group(self, group: PickupGroup, side: str) -> None: + """Move a double-clicked group to the opposite side.""" + + self._selected_key = group.key + self._selected_side = side + self._add_selected() if side == "available" else self._remove_selected() + + def _selected_group(self) -> PickupGroup | None: + """Return the group selected in the active side.""" + + source = self.available if self._selected_side == "available" else self.selected + return next((item for item in source if item.key == self._selected_key), None) + + def _add_selected(self) -> None: + """Append the selected available group to the custom sequence.""" + + if self._selected_side != "available": + return + group = self._selected_group() + if group is None: + return + self.available = [item for item in self.available if item.key != group.key] + self.selected.append(group) + self._selected_side = "selected" + self._render_lists() + + def _remove_selected(self) -> None: + """Return the selected configured group to the available list.""" + + if self._selected_side != "selected": + return + group = self._selected_group() + if group is None: + return + self.selected = [item for item in self.selected if item.key != group.key] + self.available.append(group) + self.available.sort(key=lambda item: (item.default_order, item.label)) + self._selected_side = "available" + self._render_lists() + + def _move_selected(self, offset: int) -> None: + """Move one configured group up or down.""" + + if self._selected_side != "selected": + return + index = next((i for i, item in enumerate(self.selected) if item.key == self._selected_key), -1) + target = index + int(offset) + if index < 0 or target < 0 or target >= len(self.selected): + return + self.selected[index], self.selected[target] = self.selected[target], self.selected[index] + self._render_lists() + + def _start_drag(self, group: PickupGroup, side: str) -> None: + """Remember the item being dragged.""" + + self._drag_group = group + self._drag_side = side + + @staticmethod + def _is_descendant(widget: tk.Misc | None, ancestor: tk.Misc) -> bool: + """Return whether a widget belongs to one list panel.""" + + current = widget + while current is not None: + if current == ancestor: + return True + current = getattr(current, "master", None) + return False + + def _finish_drag(self, event) -> None: + """Drop the dragged group into a list and place it near the pointer.""" + + group = self._drag_group + if group is None: + return + self._drag_group = None + try: + target_widget = self.winfo_containing(event.x_root, event.y_root) + except Exception: + return + if self._is_descendant(target_widget, self.selected_panel): + self.available = [item for item in self.available if item.key != group.key] + self.selected = [item for item in self.selected if item.key != group.key] + target_y = event.y_root - self.selected_list.winfo_rooty() + row_height = 42 + target_index = max(0, min(len(self.selected), target_y // row_height)) + self.selected.insert(target_index, group) + self._selected_side = "selected" + elif self._is_descendant(target_widget, self.available_panel): + self.selected = [item for item in self.selected if item.key != group.key] + if not any(item.key == group.key for item in self.available): + self.available.append(group) + self.available.sort(key=lambda item: (item.default_order, item.label)) + self._selected_side = "available" + else: + return + self._selected_key = group.key + self._render_lists() + + def _save_sql(self) -> tuple[str, dict[str, Any]]: + """Build one atomic soft-delete/insert batch using bound parameters.""" + + params: dict[str, Any] = { + "documento": self.documento, + "utente": self.session.login if self.session else "SYSTEM", + } + values: list[str] = [] + for index, group in enumerate(self.selected): + params[f"key_{index}"] = group.key + params[f"label_{index}"] = group.label + params[f"order_{index}"] = index + values.append( + f"(:documento, :key_{index}, :label_{index}, :order_{index}, :utente, SYSDATETIME(), :utente, SYSDATETIME())" + ) + insert = "" + if values: + insert = """ +INSERT INTO dbo.py_PickingListPrelievoSequenza ( + Documento, GruppoChiave, GruppoEtichetta, Ordine, + InsUtente, InsDataOra, ModUtente, ModDataOra +) +VALUES +""" + ",\n".join(values) + ";" + sql = f""" +SET NOCOUNT ON; +SET XACT_ABORT ON; +UPDATE dbo.py_PickingListPrelievoSequenza +SET DelUtente = :utente, + DelDataOra = SYSDATETIME(), + ModUtente = :utente, + ModDataOra = SYSDATETIME() +WHERE Documento = :documento + AND DelDataOra IS NULL; +{insert} +SELECT CAST({len(values)} AS int) AS ElementiSalvati; +""" + return sql, params + + def _save(self) -> None: + """Persist the configured prefix atomically.""" + + sql, params = self._save_sql() + + async def _job(): + return await self.db_client.query_json(sql, params, commit=True) + + self.runner.run( + _job(), + on_success=lambda _result: self._save_completed("saved"), + on_error=self._save_error, + busy=self.busy, + message=self._t("picking.sequence.busy.save", "Salvo la sequenza..."), + ) + + def _reset_default(self) -> None: + """Delete the custom sequence so the shared default becomes active.""" + + async def _job(): + return await self.db_client.query_json( + SQL_RESET_SEQUENCE, + { + "documento": self.documento, + "utente": self.session.login if self.session else "SYSTEM", + }, + commit=True, + ) + + self.runner.run( + _job(), + on_success=lambda _result: self._save_completed("reset"), + on_error=self._save_error, + busy=self.busy, + message=self._t("picking.sequence.busy.save", "Salvo la sequenza..."), + ) + + def _save_completed(self, mode: str) -> None: + """Refresh the editor and notify the parent after a successful save.""" + + message_key = "picking.sequence.msg.reset" if mode == "reset" else "picking.sequence.msg.saved" + default = ( + "Sequenza personalizzata rimossa. E' attivo l'ordine predefinito." + if mode == "reset" + else "Sequenza salvata." + ) + messagebox.showinfo(self._t("picking.sequence.title", "Sequenza di prelievo"), self._t(message_key, default), parent=self) + if self.on_saved: + self.on_saved() + self._selected_key = None + self._selected_side = None + self._load() + + def _save_error(self, exc: BaseException) -> None: + """Show a localized save error.""" + + messagebox.showerror( + self._t("picking.sequence.title", "Sequenza di prelievo"), + self._t("picking.sequence.msg.save_error", "Errore durante il salvataggio:\n{error}", error=exc), + parent=self, + ) + + +def open_pickinglist_sequence_window( + parent: tk.Misc, + db_client, + *, + documento: str, + session: UserSession | None, + on_saved: Callable[[], None] | None = None, +) -> PickingListSequenceWindow: + """Open at most one sequence editor for the picking-list window.""" + + key = "_pickinglist_sequence_window_singleton" + existing = getattr(parent, key, None) + if existing is not None: + try: + if existing.winfo_exists() and existing.documento == str(documento).strip(): + existing.on_saved = on_saved + existing.deiconify() + existing.lift() + existing.focus_force() + return existing + if existing.winfo_exists(): + existing.destroy() + except Exception: + pass + + window = PickingListSequenceWindow( + parent, + db_client, + documento=documento, + session=session, + on_saved=on_saved, + ) + setattr(parent, key, window) + + def _clear_reference() -> None: + try: + if getattr(parent, key, None) is window: + setattr(parent, key, None) + finally: + window.destroy() + + window.protocol("WM_DELETE_WINDOW", _clear_reference) + return window diff --git a/rollback_pickinglist_days_filter_patch.sql b/rollback_pickinglist_days_filter_patch.sql new file mode 100644 index 0000000..da60ea4 --- /dev/null +++ b/rollback_pickinglist_days_filter_patch.sql @@ -0,0 +1,76 @@ +/* +Rollback: intervallo giorni selezionabile in Gestione Picking List Python + +Effetto: +- ripristina le viste Python precedenti, appoggiate a dbo.XMag_ViewPackingList; +- il filtro effettivo torna quindi a quello legacy fisso a 10 giorni. +*/ + +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingList] +AS +SELECT TOP 10000 + legacy.Pallet, + legacy.Lotto, + legacy.Articolo, + legacy.Descrizione, + legacy.Qta, + legacy.Documento, + legacy.CodNazione, + legacy.NAZIONE, + legacy.Stato, + legacy.PalletCella, + legacy.Magazzino, + legacy.Area, + legacy.Cella, + legacy.Ordinamento, + legacy.Ubicazione, + legacy.DEST, + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(legacy.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END AS IDStato +FROM dbo.XMag_ViewPackingList AS legacy +LEFT JOIN dbo.PyPickingListReservation AS pr + ON pr.ID = 1 + AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL +ORDER BY + CASE + WHEN pr.Documento IS NOT NULL + AND pr.Documento = CAST(legacy.Documento AS varchar(8)) + THEN 1 + ELSE 0 + END DESC, + legacy.Documento, + legacy.Ordinamento; +GO + +CREATE OR ALTER VIEW [dbo].[py_ViewPackingListRestante] +AS +SELECT + Pallet, + Lotto, + Articolo, + Descrizione, + Qta, + Documento, + CodNazione, + NAZIONE, + Stato, + PalletCella, + Magazzino, + Area, + Cella, + Ordinamento, + Ubicazione, + DEST, + IDStato +FROM dbo.py_XMag_ViewPackingList +WHERE Cella <> 9999; +GO diff --git a/rollback_pickinglist_sequence_patch.sql b/rollback_pickinglist_sequence_patch.sql new file mode 100644 index 0000000..f8bf7ce --- /dev/null +++ b/rollback_pickinglist_sequence_patch.sql @@ -0,0 +1,19 @@ +/* +Rollback della sequenza personalizzata per picking list. + +Attenzione: +- elimina le configurazioni di sequenza salvate nel ramo Python; +- non modifica gli oggetti legacy C#; +- dopo il rollback occorre usare una versione Python precedente alla feature. +*/ + +IF OBJECT_ID(N'dbo.py_ViewPackingListPrelievo', N'V') IS NOT NULL + DROP VIEW dbo.py_ViewPackingListPrelievo; +GO + +IF OBJECT_ID(N'dbo.py_PickingListPrelievoSequenza', N'U') IS NOT NULL + DROP TABLE dbo.py_PickingListPrelievoSequenza; +GO + +PRINT 'Rollback sequenza prelievo Python completato.'; +GO diff --git a/specifica_sequenza_prelievo.md b/specifica_sequenza_prelievo.md new file mode 100644 index 0000000..91544fa --- /dev/null +++ b/specifica_sequenza_prelievo.md @@ -0,0 +1,74 @@ +# Sequenza di prelievo per picking list + +## Obiettivo + +Consentire al responsabile di definire, per una singola picking list, l'ordine +dei gruppi di ubicazioni che il barcode deve seguire. Il barcode continua a +presentare una sola UDC alla volta e non espone la configurazione all'operatore. + +## Ordine predefinito + +In assenza di una configurazione personalizzata il sistema propone: + +1. UDC `Non scaff.` nella cella convenzionale `1000` / `5E.1.1`. +2. UDC negli scaffali secondo il valore legacy `Celle.Ordinamento`. + +L'ordinamento globale delle celle non viene modificato. + +## Configurazione per documento + +La finestra `Sequenza di prelievo` si apre dalla picking list selezionata in +`Gestione Picking List`. I gruppi disponibili sono `Non scaff.` e le corsie +effettivamente presenti nel contenuto residuo del documento. + +L'utente puo' trascinare i gruppi nell'area destra, usare il doppio clic oppure +i pulsanti `Aggiungi`, `Rimuovi`, `Su` e `Giu'`. L'ordine dall'alto verso il +basso definisce la precedenza dei gruppi configurati. + +Una sequenza puo' essere parziale: gli eventuali gruppi non inseriti vengono +accodati secondo l'ordine predefinito. In questo modo una configurazione non +puo' rendere invisibili UDC o scaffali aggiunti successivamente alla picking +list. + +`Ripristina default` elimina la configurazione del documento e riattiva +immediatamente l'ordine standard. + +## Persistenza e concorrenza + +La tabella Python-only `dbo.py_PickingListPrelievoSequenza` memorizza una riga +per documento e gruppo, con ordine e campi di audit. Il salvataggio marca come +cancellata la configurazione precedente e inserisce quella nuova in una singola +transazione con `XACT_ABORT ON`, conservando quindi anche lo storico delle +variazioni. + +Due picking list possono quindi avere sequenze diverse senza modificare le +celle, le altre liste o gli oggetti usati dal programma C#. + +## Consumo dal barcode + +La vista Python-only `dbo.py_ViewPackingListPrelievo` deriva dalla vista +residuale, esclude quindi le UDC gia' in `7G.1.1` e calcola +`OrdinePrelievoGruppo`. Sia la griglia di dettaglio sia il repository barcode +ordinano per: + +1. `OrdinePrelievoGruppo`; +2. `Ordinamento` legacy della cella; +3. codice pallet come criterio stabile finale. + +Pausa, ripresa, salto UDC, F1/F2 e prenotazione non vengono modificati. Quando +una UDC viene movimentata, la richiesta successiva rilegge la vista e usa la +sequenza corrente sul contenuto residuo. + +## Installazione e rollback + +Per un aggiornamento completo del database online usare +`apply_online_python_wms_full_patch.sql`. + +Per applicare solo questa evoluzione a un database gia' aggiornato: + +1. eseguire `apply_pickinglist_days_filter_patch.sql`; +2. eseguire `apply_pickinglist_sequence_patch.sql`. + +Il rollback specifico e' `rollback_pickinglist_sequence_patch.sql`; dopo il +rollback deve essere ripristinata anche una versione applicativa precedente, +poiche' il nuovo codice interroga la vista di prelievo. diff --git a/tooltip.json b/tooltip.json index f54f8ed..bf07cad 100644 --- a/tooltip.json +++ b/tooltip.json @@ -33,7 +33,15 @@ "multi_udc.collapse_all": "Comprime tutti i nodi dell'albero.", "multi_udc.preselect": "Espande la corsia selezionata e preseleziona automaticamente le UDC con causale fantasma.", "multi_udc.remove_ghosts": "Scarica logicamente le UDC selezionate della corsia attiva verso l'ubicazione di uscita.", - "multi_udc.export_xlsx": "Esporta in Excel il contenuto corrente della vista UDC fantasma." + "multi_udc.export_xlsx": "Esporta in Excel il contenuto corrente della vista UDC fantasma.", + "picking.sequence.item": "Seleziona il gruppo. Puoi trascinarlo nell'altra area oppure usare doppio clic e pulsanti centrali.", + "picking.sequence.add": "Aggiunge il gruppo selezionato alla fine della sequenza personalizzata.", + "picking.sequence.remove": "Rimuove il gruppo dalla sequenza personalizzata e lo riporta tra quelli disponibili.", + "picking.sequence.up": "Sposta il gruppo selezionato una posizione verso l'alto.", + "picking.sequence.down": "Sposta il gruppo selezionato una posizione verso il basso.", + "picking.sequence.save": "Salva in modo atomico la sequenza per la picking list corrente.", + "picking.sequence.reset": "Elimina la personalizzazione e ripristina Non scaff. prima degli scaffali.", + "picking.sequence.close": "Chiude la finestra senza effettuare altri salvataggi." }, "ENG": { "launcher.open_reset_corsie": "Open the aisle management window to inspect an aisle and empty it in a controlled way.", @@ -68,6 +76,14 @@ "multi_udc.collapse_all": "Collapse all tree nodes.", "multi_udc.preselect": "Expand the selected aisle and automatically preselect UDCs classified as ghost candidates.", "multi_udc.remove_ghosts": "Logically unload the selected UDCs of the active aisle to the outbound location.", - "multi_udc.export_xlsx": "Export the current ghost UDC view to Excel." + "multi_udc.export_xlsx": "Export the current ghost UDC view to Excel.", + "picking.sequence.item": "Select the group. You can drag it to the other area or use double-click and the center buttons.", + "picking.sequence.add": "Append the selected group to the custom sequence.", + "picking.sequence.remove": "Remove the group from the custom sequence and return it to the available list.", + "picking.sequence.up": "Move the selected group one position up.", + "picking.sequence.down": "Move the selected group one position down.", + "picking.sequence.save": "Atomically save the sequence for the current picking list.", + "picking.sequence.reset": "Remove the customization and restore non-shelved before racks.", + "picking.sequence.close": "Close the window without further saves." } } diff --git a/ui_theme.json b/ui_theme.json index 5af92fa..fdaf65f 100644 --- a/ui_theme.json +++ b/ui_theme.json @@ -301,6 +301,29 @@ "overlay_label_padding": [18, 14, 18, 8], "overlay_progress_padding": [18, 0, 18, 14] }, + "picking_sequence_window": { + "window_fg_color": ["#eef2f3", "#2f2f2f"], + "list_fg_color": ["#f8fafc", "#363636"], + "item_fg_color": "#2ebf74", + "secondary_text_color": "#4b5563", + "reset_button_fg_color": "#6b7280", + "title_font": { + "family": "Segoe UI", + "size": 16, + "weight": "bold" + }, + "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], + "overlay_panel_fg_color": ["#f2f2f2", "#353535"], + "overlay_panel_corner_radius": 10, + "overlay_label_font": { + "family": "Segoe UI", + "size": 11, + "weight": "bold" + }, + "overlay_progress_width": 220, + "overlay_label_padding": [18, 14, 18, 8], + "overlay_progress_padding": [18, 0, 18, 14] + }, "history_picking_window": { "window_geometry": "1200x720", "window_minsize": [980, 560], diff --git a/user_session.py b/user_session.py index 87727fe..fbfcb6c 100644 --- a/user_session.py +++ b/user_session.py @@ -40,6 +40,7 @@ ALL_ACTIONS: FrozenSet[str] = frozenset( "pickinglist.view", "pickinglist.prenota", "pickinglist.sprenota", + "pickinglist.sequence", } ) diff --git a/version_info.py b/version_info.py index 7cf0b76..52fa8b4 100644 --- a/version_info.py +++ b/version_info.py @@ -14,18 +14,19 @@ MODULE_VERSIONS: dict[str, str] = { "audit_log": "1.0.0", "main": "1.0.2", "barcode_client": "1.0.26", - "barcode_repository": "1.0.12", + "barcode_repository": "1.0.13", "barcode_service": "1.0.24", "busy_overlay": "1.0.0", "db_config": "1.0.0", "diagnostica": "1.0.4", "gestione_aree": "1.0.1", "gestione_layout": "1.0.2", - "gestione_pickinglist": "1.0.4", + "gestione_pickinglist": "1.0.6", "gestione_scarico": "1.0.8", "locale_text": "1.0.0", "login_window": "1.0.0", "prenota_sprenota_sql": "1.0.1", + "pickinglist_sequence": "1.0.0", "reset_corsie": "1.0.0", "runtime_support": "1.0.1", "search_pallets": "1.0.0", @@ -34,7 +35,7 @@ MODULE_VERSIONS: dict[str, str] = { "tooltips": "1.0.0", "udc_non_scaffalate": "1.0.1", "ui_theme": "1.0.0", - "user_session": "1.0.1", + "user_session": "1.0.2", "view_celle_multi_udc": "1.0.0", "window_placement": "1.0.0", }