Release storico UDC e picking list
This commit is contained in:
61
INSTALLAZIONE.md
Normal file
61
INSTALLAZIONE.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Installazione Warehouse Python
|
||||
|
||||
## Requisiti
|
||||
|
||||
1. Windows 11 o Windows Server 2019.
|
||||
2. Python 3.13 o versione compatibile installata e presente nel `PATH`.
|
||||
3. Microsoft ODBC Driver per SQL Server installato sul PC.
|
||||
- Driver consigliato: `ODBC Driver 17 for SQL Server`.
|
||||
4. Accesso di rete al server SQL.
|
||||
|
||||
## Installazione dipendenze
|
||||
|
||||
Aprire PowerShell nella cartella del programma ed eseguire:
|
||||
|
||||
```powershell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Avvio backoffice
|
||||
|
||||
```powershell
|
||||
python main.py
|
||||
```
|
||||
|
||||
Al primo avvio, se manca `db_connection.json`, il programma mostra la finestra di configurazione database.
|
||||
|
||||
Per avviare senza finestra console:
|
||||
|
||||
```powershell
|
||||
pythonw warehouse.pyw
|
||||
```
|
||||
|
||||
## Avvio client barcode
|
||||
|
||||
```powershell
|
||||
python barcode_client.py
|
||||
```
|
||||
|
||||
Per avviare senza finestra console:
|
||||
|
||||
```powershell
|
||||
pythonw barcode.pyw
|
||||
```
|
||||
|
||||
## Patch DB Picking List
|
||||
|
||||
Per usare in parallelo C# legacy e Python, eseguire in SSMS:
|
||||
|
||||
```text
|
||||
apply_python_parallel_pickinglist_patch.sql
|
||||
```
|
||||
|
||||
Questa patch crea solo oggetti dedicati al Python con prefisso `py_` e non modifica gli oggetti legacy usati dal C#.
|
||||
|
||||
Per rimuovere gli oggetti Python dopo i test, eseguire:
|
||||
|
||||
```text
|
||||
rollback_python_parallel_pickinglist_patch.sql
|
||||
```
|
||||
|
||||
La vecchia patch `apply_plist_reservation_patch.sql` modifica invece gli oggetti legacy: usarla solo se si vuole sostituire temporaneamente il comportamento del C#.
|
||||
182
apply_online_history_forms_patch.sql
Normal file
182
apply_online_history_forms_patch.sql
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Patch online - form storiche Python
|
||||
|
||||
Contenuto:
|
||||
- crea/aggiorna le viste Python-only usate dalla form "Storico Picking List"
|
||||
- non modifica stored procedure legacy C#
|
||||
- non modifica le viste legacy C#
|
||||
- non crea oggetti per "Storico movimenti UDC", perche' quella form legge in sola lettura:
|
||||
dbo.MagazziniPallet, dbo.Celle, dbo.XMag_GiacenzaPalletPlistChiuse
|
||||
|
||||
Oggetti creati/aggiornati:
|
||||
- dbo.py_vPreparaPackingListSAMA1
|
||||
- dbo.py_vPreparaPackingList
|
||||
- dbo.py_XMag_ViewPackingListStorico
|
||||
*/
|
||||
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[PyPickingListReservation](
|
||||
[ID] [tinyint] NOT NULL,
|
||||
[Documento] [varchar](8) NULL,
|
||||
[IDOperatore] [int] NULL,
|
||||
[ModUtente] [varchar](50) NULL,
|
||||
[ModDataOra] [datetime] NULL,
|
||||
CONSTRAINT [PK_PyPickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||
CONSTRAINT [CK_PyPickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||
) ON [PRIMARY];
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||
BEGIN
|
||||
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingListSAMA1]
|
||||
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.BAMTES.STATO AS StatoDocumento,
|
||||
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
|
||||
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 IN ('P', 'D')
|
||||
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.BAMTES.STATO,
|
||||
SAMA1.dbo.NAZIONI.CODICE,
|
||||
SAMA1.dbo.NAZIONI.DESCR,
|
||||
SAMA1.dbo.BAMTES.IDMTRASP,
|
||||
SAMA1.dbo.MTRASP.CODICE;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingList]
|
||||
AS
|
||||
SELECT
|
||||
NUMLOT,
|
||||
CODICE,
|
||||
DESCR,
|
||||
UDC,
|
||||
Qta,
|
||||
NUMDOC,
|
||||
DATDOC,
|
||||
ID,
|
||||
DESCRDEST,
|
||||
StatoDocumento,
|
||||
Expr1,
|
||||
NAZIONE
|
||||
FROM dbo.py_vPreparaPackingListSAMA1
|
||||
WHERE NUMLOT <> '';
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingListStorico]
|
||||
AS
|
||||
SELECT TOP (100000)
|
||||
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.StatoDocumento,
|
||||
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 c
|
||||
INNER JOIN dbo.XMag_GiacenzaPallet g
|
||||
ON c.ID = g.IDCella
|
||||
RIGHT OUTER JOIN dbo.py_vPreparaPackingList prep
|
||||
ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC
|
||||
LEFT JOIN dbo.PyPickingListReservation pr
|
||||
ON pr.ID = 1
|
||||
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||
GROUP BY
|
||||
prep.Expr1,
|
||||
prep.NAZIONE,
|
||||
prep.UDC,
|
||||
prep.NUMDOC,
|
||||
prep.DATDOC,
|
||||
prep.StatoDocumento,
|
||||
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
|
||||
|
||||
PRINT 'Patch form storiche Python applicata.';
|
||||
PRINT 'Creato/aggiornato dbo.py_vPreparaPackingListSAMA1';
|
||||
PRINT 'Creato/aggiornato dbo.py_vPreparaPackingList';
|
||||
PRINT 'Creato/aggiornato dbo.py_XMag_ViewPackingListStorico';
|
||||
GO
|
||||
328
apply_python_parallel_pickinglist_patch.sql
Normal file
328
apply_python_parallel_pickinglist_patch.sql
Normal file
@@ -0,0 +1,328 @@
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
|
||||
/*
|
||||
Patch parallela per usare C# e Python contemporaneamente.
|
||||
|
||||
Questa patch NON modifica gli oggetti legacy usati dal C#.
|
||||
|
||||
Il C# continua a usare:
|
||||
- dbo.XMag_ViewPackingList
|
||||
- dbo.ViewPackingListRestante
|
||||
- dbo.sp_xExePackingListPallet
|
||||
- dbo.sp_xExePackingListPalletPrenota
|
||||
- dbo.sp_ControllaPrenotazionePackingListPalletNew
|
||||
|
||||
Il Python usa invece:
|
||||
- dbo.py_XMag_ViewPackingList
|
||||
- dbo.py_ViewPackingListRestante
|
||||
- dbo.py_sp_xExePackingListPallet
|
||||
- dbo.py_sp_xExePackingListPalletPrenota
|
||||
- dbo.py_sp_ControllaPrenotazionePackingListPalletNew
|
||||
- dbo.PyPickingListReservation
|
||||
*/
|
||||
|
||||
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[PyPickingListReservation](
|
||||
[ID] [tinyint] NOT NULL,
|
||||
[Documento] [varchar](8) NULL,
|
||||
[IDOperatore] [int] NULL,
|
||||
[ModUtente] [varchar](50) NULL,
|
||||
[ModDataOra] [datetime] NULL,
|
||||
CONSTRAINT [PK_PyPickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||
CONSTRAINT [CK_PyPickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||
) ON [PRIMARY];
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||
BEGIN
|
||||
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||
END
|
||||
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 legacy
|
||||
LEFT JOIN dbo.PyPickingListReservation 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
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[py_sp_xExePackingListPallet]
|
||||
@IDOperatore int,
|
||||
@Documento varchar(8),
|
||||
@Azione char(1) = 'P',
|
||||
@RC int OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET @RC = 0;
|
||||
|
||||
DECLARE @Nominativo varchar(50) = '';
|
||||
DECLARE @DocumentoAttivo varchar(8) = NULL;
|
||||
DECLARE @Description varchar(255) = '';
|
||||
DECLARE @Message varchar(255) = '';
|
||||
DECLARE @IDResult int = 0;
|
||||
DECLARE @ID int = 0;
|
||||
|
||||
SELECT @Nominativo = [Login]
|
||||
FROM dbo.Operatori
|
||||
WHERE ID = @IDOperatore;
|
||||
|
||||
IF @Nominativo IS NULL
|
||||
SET @Nominativo = 'SYSTEM';
|
||||
|
||||
IF @Azione NOT IN ('P', 'S')
|
||||
BEGIN
|
||||
SET @RC = -10;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.py_XMag_ViewPackingList
|
||||
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||
)
|
||||
BEGIN
|
||||
SET @RC = -20;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||
BEGIN
|
||||
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||
END;
|
||||
|
||||
SELECT @DocumentoAttivo = NULLIF(LTRIM(RTRIM(Documento)), '')
|
||||
FROM dbo.PyPickingListReservation
|
||||
WHERE ID = 1;
|
||||
|
||||
DECLARE @TargetCelle TABLE (
|
||||
IDCella int PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO @TargetCelle (IDCella)
|
||||
SELECT DISTINCT Cella
|
||||
FROM dbo.py_XMag_ViewPackingList
|
||||
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||
AND Cella IS NOT NULL;
|
||||
|
||||
IF @Azione = 'P'
|
||||
BEGIN
|
||||
IF @DocumentoAttivo = @Documento
|
||||
RETURN;
|
||||
|
||||
UPDATE c
|
||||
SET c.IDStato = 0,
|
||||
c.ModUtente = @Nominativo,
|
||||
c.ModDataOra = GETDATE()
|
||||
FROM dbo.Celle c
|
||||
WHERE ISNULL(c.IDStato, 0) <> 0;
|
||||
|
||||
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;
|
||||
|
||||
UPDATE dbo.PyPickingListReservation
|
||||
SET Documento = @Documento,
|
||||
IDOperatore = @IDOperatore,
|
||||
ModUtente = @Nominativo,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ID = 1;
|
||||
|
||||
SELECT TOP 1 @Description = NAZIONE
|
||||
FROM dbo.py_XMag_ViewPackingList
|
||||
WHERE CAST(Documento AS varchar(8)) = @Documento;
|
||||
|
||||
EXEC dbo.sp_LogPackingList
|
||||
@ID = @ID,
|
||||
@Code = @Documento,
|
||||
@Description = @Description,
|
||||
@Message = @Message OUTPUT,
|
||||
@IDResult = @IDResult OUTPUT;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF @Azione = 'S'
|
||||
BEGIN
|
||||
IF ISNULL(@DocumentoAttivo, '') <> @Documento
|
||||
RETURN;
|
||||
|
||||
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;
|
||||
|
||||
UPDATE dbo.PyPickingListReservation
|
||||
SET Documento = NULL,
|
||||
IDOperatore = @IDOperatore,
|
||||
ModUtente = @Nominativo,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ID = 1;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[py_sp_xExePackingListPalletPrenota]
|
||||
@IDOperatore int,
|
||||
@Documento varchar(8),
|
||||
@RC int OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET @RC = 0;
|
||||
|
||||
DECLARE @Nominativo varchar(50) = '';
|
||||
|
||||
SELECT @Nominativo = LOGIN
|
||||
FROM dbo.Operatori
|
||||
WHERE ID = @IDOperatore;
|
||||
|
||||
IF @Nominativo IS NULL
|
||||
SET @Nominativo = 'SYSTEM';
|
||||
|
||||
UPDATE c
|
||||
SET c.IDStato = 1,
|
||||
c.ModUtente = @Nominativo,
|
||||
c.ModDataOra = GETDATE()
|
||||
FROM dbo.Celle c
|
||||
WHERE c.ID IN (
|
||||
SELECT DISTINCT Cella
|
||||
FROM dbo.py_ViewPackingListRestante
|
||||
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||
AND Cella IS NOT NULL
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[py_sp_ControllaPrenotazionePackingListPalletNew]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @Documento varchar(8) = NULL;
|
||||
DECLARE @IDOperatore int = 0;
|
||||
DECLARE @Nominativo varchar(50) = 'SYSTEM';
|
||||
DECLARE @RC int = 0;
|
||||
|
||||
SELECT
|
||||
@Documento = NULLIF(LTRIM(RTRIM(Documento)), ''),
|
||||
@IDOperatore = ISNULL(IDOperatore, 0),
|
||||
@Nominativo = ISNULL(NULLIF(LTRIM(RTRIM(ModUtente)), ''), 'SYSTEM')
|
||||
FROM dbo.PyPickingListReservation
|
||||
WHERE ID = 1;
|
||||
|
||||
IF ISNULL(@Documento, '') = ''
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.py_ViewPackingListRestante
|
||||
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||
)
|
||||
BEGIN
|
||||
UPDATE dbo.Celle
|
||||
SET IDStato = 0,
|
||||
ModUtente = @Nominativo,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ISNULL(IDStato, 0) <> 0;
|
||||
|
||||
UPDATE dbo.PyPickingListReservation
|
||||
SET Documento = NULL,
|
||||
IDOperatore = @IDOperatore,
|
||||
ModUtente = @Nominativo,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ID = 1;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.Celle
|
||||
SET IDStato = 0,
|
||||
ModUtente = @Nominativo,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ISNULL(IDStato, 0) <> 0;
|
||||
|
||||
IF @IDOperatore <= 0
|
||||
BEGIN
|
||||
SELECT TOP 1 @IDOperatore = ID
|
||||
FROM dbo.Operatori
|
||||
WHERE LOGIN = @Nominativo;
|
||||
END;
|
||||
|
||||
EXEC dbo.py_sp_xExePackingListPalletPrenota
|
||||
@IDOperatore = @IDOperatore,
|
||||
@Documento = @Documento,
|
||||
@RC = @RC OUTPUT;
|
||||
END;
|
||||
GO
|
||||
158
apply_python_pickinglist_history_views.sql
Normal file
158
apply_python_pickinglist_history_views.sql
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Patch SQL - viste storiche picking list per ramo Python
|
||||
|
||||
Obiettivo:
|
||||
- creare un ramo di lettura storico completamente separato dalle viste C#
|
||||
- non modificare dbo.vPreparaPackingListSAMA1
|
||||
- non modificare dbo.vPreparaPackingList
|
||||
- non modificare dbo.XMag_ViewPackingList
|
||||
- non modificare stored procedure C#
|
||||
|
||||
La form Python "Storico Picking List" legge da dbo.py_XMag_ViewPackingListStorico.
|
||||
*/
|
||||
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingListSAMA1]
|
||||
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.BAMTES.STATO AS StatoDocumento,
|
||||
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
|
||||
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 IN ('P', 'D')
|
||||
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.BAMTES.STATO,
|
||||
SAMA1.dbo.NAZIONI.CODICE,
|
||||
SAMA1.dbo.NAZIONI.DESCR,
|
||||
SAMA1.dbo.BAMTES.IDMTRASP,
|
||||
SAMA1.dbo.MTRASP.CODICE;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingList]
|
||||
AS
|
||||
SELECT
|
||||
NUMLOT,
|
||||
CODICE,
|
||||
DESCR,
|
||||
UDC,
|
||||
Qta,
|
||||
NUMDOC,
|
||||
DATDOC,
|
||||
ID,
|
||||
DESCRDEST,
|
||||
StatoDocumento,
|
||||
Expr1,
|
||||
NAZIONE
|
||||
FROM dbo.py_vPreparaPackingListSAMA1
|
||||
WHERE NUMLOT <> '';
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingListStorico]
|
||||
AS
|
||||
SELECT TOP (100000)
|
||||
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.StatoDocumento,
|
||||
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 c
|
||||
INNER JOIN dbo.XMag_GiacenzaPallet g
|
||||
ON c.ID = g.IDCella
|
||||
RIGHT OUTER JOIN dbo.py_vPreparaPackingList prep
|
||||
ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC
|
||||
LEFT JOIN dbo.PyPickingListReservation pr
|
||||
ON pr.ID = 1
|
||||
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||
GROUP BY
|
||||
prep.Expr1,
|
||||
prep.NAZIONE,
|
||||
prep.UDC,
|
||||
prep.NUMDOC,
|
||||
prep.DATDOC,
|
||||
prep.StatoDocumento,
|
||||
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
|
||||
|
||||
PRINT 'Viste storiche Python create/aggiornate:';
|
||||
PRINT ' - dbo.py_vPreparaPackingListSAMA1';
|
||||
PRINT ' - dbo.py_vPreparaPackingList';
|
||||
PRINT ' - dbo.py_XMag_ViewPackingListStorico';
|
||||
GO
|
||||
@@ -7,6 +7,7 @@ singleton so every module can schedule work on the same async runtime.
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import contextlib
|
||||
|
||||
|
||||
class _LoopHolder:
|
||||
@@ -15,6 +16,7 @@ class _LoopHolder:
|
||||
def __init__(self):
|
||||
self.loop = None
|
||||
self.thread = None
|
||||
self.closing = False
|
||||
|
||||
|
||||
_GLOBAL = _LoopHolder()
|
||||
@@ -26,7 +28,7 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||
The loop is created lazily the first time the function is called and kept
|
||||
alive for the lifetime of the application.
|
||||
"""
|
||||
if _GLOBAL.loop:
|
||||
if _GLOBAL.loop and not _GLOBAL.closing:
|
||||
return _GLOBAL.loop
|
||||
|
||||
ready = threading.Event()
|
||||
@@ -35,7 +37,23 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||
_GLOBAL.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(_GLOBAL.loop)
|
||||
ready.set()
|
||||
try:
|
||||
_GLOBAL.loop.run_forever()
|
||||
finally:
|
||||
loop = _GLOBAL.loop
|
||||
if loop is not None:
|
||||
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
if pending:
|
||||
with contextlib.suppress(Exception):
|
||||
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||
with contextlib.suppress(Exception):
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
with contextlib.suppress(Exception):
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
with contextlib.suppress(Exception):
|
||||
loop.close()
|
||||
|
||||
_GLOBAL.thread = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
|
||||
_GLOBAL.thread.start()
|
||||
@@ -46,7 +64,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||
def stop_global_loop():
|
||||
"""Stop the shared loop and join the background thread if present."""
|
||||
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
||||
_GLOBAL.closing = True
|
||||
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
||||
_GLOBAL.thread.join(timeout=2)
|
||||
_GLOBAL.loop = None
|
||||
_GLOBAL.thread = None
|
||||
_GLOBAL.closing = False
|
||||
|
||||
@@ -19,6 +19,16 @@ from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
try:
|
||||
import pyodbc
|
||||
|
||||
# The desktop app opens short-lived SQL connections from a background
|
||||
# asyncio loop. ODBC pooling can keep native handles alive for a while after
|
||||
# the GUI closes, which is especially visible with pythonw.
|
||||
pyodbc.pooling = False
|
||||
except Exception:
|
||||
pyodbc = None # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
import orjson as _json
|
||||
|
||||
|
||||
8
barcode.pyw
Normal file
8
barcode.pyw
Normal file
@@ -0,0 +1,8 @@
|
||||
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||
|
||||
ensure_stdio("warehouse_barcode")
|
||||
|
||||
from barcode_client import main
|
||||
|
||||
|
||||
raise SystemExit(run_with_fatal_log("Barcode WMS", main))
|
||||
@@ -9,6 +9,10 @@ from concurrent.futures import Future
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import Callable
|
||||
|
||||
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||
|
||||
ensure_stdio("warehouse_barcode")
|
||||
|
||||
from async_loop_singleton import get_global_loop, stop_global_loop
|
||||
from async_msssql_query import AsyncMSSQLClient
|
||||
from barcode_repository import BarcodeRepository
|
||||
@@ -17,13 +21,6 @@ from db_config import build_dsn_from_config, ensure_db_config
|
||||
from login_window import prompt_login_compact
|
||||
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
try:
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class BarcodeClientApp:
|
||||
"""Single-window Tk barcode client modeled after the C# legacy form."""
|
||||
|
||||
@@ -478,4 +475,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(run_with_fatal_log("Barcode WMS", main))
|
||||
|
||||
@@ -21,7 +21,7 @@ SELECT TOP (1)
|
||||
Ubicazione,
|
||||
Ordinamento,
|
||||
IDStato
|
||||
FROM dbo.XMag_ViewPackingList
|
||||
FROM dbo.py_XMag_ViewPackingList
|
||||
WHERE Ordinamento > 0
|
||||
AND IDStato = :id_stato
|
||||
ORDER BY Ordinamento;
|
||||
@@ -38,7 +38,7 @@ SELECT TOP (1)
|
||||
Ubicazione,
|
||||
Ordinamento,
|
||||
IDStato
|
||||
FROM dbo.XMag_ViewPackingList
|
||||
FROM dbo.py_XMag_ViewPackingList
|
||||
WHERE Pallet = :pallet
|
||||
ORDER BY Ordinamento;
|
||||
"""
|
||||
@@ -65,6 +65,8 @@ EXEC dbo.sp_xMagGestioneMagazziniPallet
|
||||
@NumeroCella = :numero_cella,
|
||||
@RC = @RC OUTPUT;
|
||||
|
||||
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||
|
||||
SELECT
|
||||
@RC AS RC,
|
||||
:barcode_cella AS BarcodeCella,
|
||||
|
||||
69
checklist_test_campo_pickinglist.md
Normal file
69
checklist_test_campo_pickinglist.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Checklist Test Campo Picking List
|
||||
|
||||
## Deploy patch
|
||||
|
||||
1. Apri [apply_plist_reservation_patch.sql](C:/devel/python/ware_house/apply_plist_reservation_patch.sql) in SSMS.
|
||||
2. Verifica di essere collegato al database corretto.
|
||||
3. Esegui lo script completo.
|
||||
4. Controlla che l'esecuzione termini senza errori SQL.
|
||||
|
||||
## Verifica oggetti DB
|
||||
|
||||
1. Verifica che esista la tabella `dbo.PickingListReservation`.
|
||||
2. Verifica che siano stati aggiornati questi oggetti:
|
||||
- `dbo.sp_xExePackingListPallet`
|
||||
- `dbo.XMag_ViewPackingList`
|
||||
- `dbo.sp_xExePackingListPalletPrenota`
|
||||
- `dbo.sp_ControllaPrenotazionePackingListPalletNew`
|
||||
3. Verifica che in `dbo.WarehouseObjectBackup` esistano le copie con tag `plist_reservation_fix_alpha2`.
|
||||
|
||||
## Test backoffice Gestione Picking List
|
||||
|
||||
1. Apri `Gestione Picking List`.
|
||||
2. Verifica che la griglia alta mostri solo le UDC residue.
|
||||
3. Prenota una picking list.
|
||||
4. Verifica che risulti prenotata solo quella.
|
||||
5. Premi di nuovo `Prenota` sulla stessa lista.
|
||||
- Atteso: non cambia nulla.
|
||||
6. Premi `S-prenota` sulla lista prenotata.
|
||||
- Atteso: la lista passa a non prenotata.
|
||||
7. Premi di nuovo `S-prenota`.
|
||||
- Atteso: non cambia nulla.
|
||||
8. Prenota una lista con UDC non scaffalate.
|
||||
- Atteso: non devono accendersi altre liste per effetto della locazione `1000 / 5E1.1`.
|
||||
9. Prenota una lista che in passato collideva sulla cella `8057`.
|
||||
- Atteso: non deve più prenotare anche l'altra.
|
||||
|
||||
## Test barcode
|
||||
|
||||
1. Con una sola lista prenotata, premi `F1`.
|
||||
- Atteso: viene proposta la coda alta della lista prenotata.
|
||||
2. Premi `F2`.
|
||||
- Atteso: viene proposta la coda bassa non prenotata.
|
||||
3. Preleva una UDC della picking list.
|
||||
- Atteso: compare `Ok Scarico`.
|
||||
- Atteso: compare `7G.1.1`.
|
||||
4. Se la UDC è non scaffalata:
|
||||
- Atteso: compare `5E1.1`.
|
||||
5. Dopo il prelievo, se restano altre UDC della stessa plist:
|
||||
- Atteso: compare la UDC successiva.
|
||||
|
||||
## Test fine lista
|
||||
|
||||
1. Lavora una picking list fino all'ultima UDC.
|
||||
2. Verifica che la plist sparisca dalla griglia alta.
|
||||
3. Verifica che la prenotazione venga automaticamente disattivata.
|
||||
4. Verifica che `F1` non continui più a considerarla attiva.
|
||||
|
||||
## Test Ricarica
|
||||
|
||||
1. Durante il lavoro su una plist, premi `Ricarica`.
|
||||
2. Verifica che la griglia alta mostri solo il residuo reale.
|
||||
3. Verifica che la plist esaurita non ricompaia.
|
||||
|
||||
## Rollback
|
||||
|
||||
1. Apri [rollback_plist_reservation_patch.sql](C:/devel/python/ware_house/rollback_plist_reservation_patch.sql) in SSMS.
|
||||
2. Verifica di essere sullo stesso database usato per il deploy.
|
||||
3. Esegui lo script completo.
|
||||
4. Controlla che il comportamento legacy sia ripristinato.
|
||||
@@ -215,14 +215,14 @@ SELECT
|
||||
MAX(Cella) AS Cella,
|
||||
MIN(Ordinamento) AS Ordinamento,
|
||||
MAX(IDStato) AS IDStato
|
||||
FROM dbo.ViewPackingListRestante
|
||||
FROM dbo.py_ViewPackingListRestante
|
||||
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
||||
"""
|
||||
|
||||
SQL_PL_DETAILS = """
|
||||
SELECT *
|
||||
FROM ViewPackingListRestante
|
||||
FROM dbo.py_ViewPackingListRestante
|
||||
WHERE Documento = :Documento
|
||||
ORDER BY Ordinamento;
|
||||
"""
|
||||
|
||||
@@ -353,7 +353,7 @@ FROM (
|
||||
WHERE c.ID = :target_idcella
|
||||
) AS target;
|
||||
|
||||
EXEC dbo.sp_ControllaPrenotazionePackingListPalletNew;
|
||||
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||
|
||||
SELECT
|
||||
CAST(1 AS int) AS Ok,
|
||||
|
||||
32
locale.json
32
locale.json
@@ -7,7 +7,9 @@
|
||||
"launcher.layout": "Gestione Layout",
|
||||
"launcher.multi_udc": "UDC Fantasma",
|
||||
"launcher.search": "Ricerca UDC",
|
||||
"launcher.history_udc": "Storico movimenti UDC",
|
||||
"launcher.pickinglist": "Gestione Picking List",
|
||||
"launcher.history_pickinglist": "Storico Picking List",
|
||||
"launcher.arrange": "Ridisponi finestre",
|
||||
"launcher.exit": "Esci",
|
||||
"launcher.already_running_title": "Warehouse",
|
||||
@@ -59,6 +61,20 @@
|
||||
"search.msg.no_results": "Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
||||
"search.msg.error_title": "Errore ricerca",
|
||||
"search.busy": "Cerco...",
|
||||
"history.udc.title": "Storico movimenti UDC",
|
||||
"history.udc.button.search": "Cerca",
|
||||
"history.udc.busy": "Carico storico UDC...",
|
||||
"history.udc.msg.title": "Storico UDC",
|
||||
"history.udc.msg.empty": "Nessun movimento trovato.",
|
||||
"history.udc.msg.error": "Errore ricerca:\n{error}",
|
||||
"history.picking.title": "Storico Picking List",
|
||||
"history.picking.button.reload": "Ricarica",
|
||||
"history.picking.detail_title": "Dettaglio contenuto",
|
||||
"history.picking.busy.master": "Carico storico picking list...",
|
||||
"history.picking.busy.detail": "Carico dettaglio picking list...",
|
||||
"history.picking.msg.title": "Storico Picking List",
|
||||
"history.picking.msg.load_error": "Errore caricamento:\n{error}",
|
||||
"history.picking.msg.detail_error": "Errore dettaglio:\n{error}",
|
||||
"reset.title": "Gestione Corsie - svuotamento celle per corsia",
|
||||
"reset.label.aisle": "Corsia:",
|
||||
"reset.button.refresh": "Carica",
|
||||
@@ -104,7 +120,9 @@
|
||||
"launcher.layout": "Layout Management",
|
||||
"launcher.multi_udc": "Ghost UDC",
|
||||
"launcher.search": "UDC Search",
|
||||
"launcher.history_udc": "UDC Movement History",
|
||||
"launcher.pickinglist": "Picking List Management",
|
||||
"launcher.history_pickinglist": "Picking List History",
|
||||
"launcher.arrange": "Arrange windows",
|
||||
"launcher.exit": "Exit",
|
||||
"launcher.already_running_title": "Warehouse",
|
||||
@@ -156,6 +174,20 @@
|
||||
"search.msg.no_results": "No matches were found for the entered search keys.",
|
||||
"search.msg.error_title": "Search error",
|
||||
"search.busy": "Searching...",
|
||||
"history.udc.title": "UDC Movement History",
|
||||
"history.udc.button.search": "Search",
|
||||
"history.udc.busy": "Loading UDC history...",
|
||||
"history.udc.msg.title": "UDC History",
|
||||
"history.udc.msg.empty": "No movement found.",
|
||||
"history.udc.msg.error": "Search failed:\n{error}",
|
||||
"history.picking.title": "Picking List History",
|
||||
"history.picking.button.reload": "Reload",
|
||||
"history.picking.detail_title": "Content detail",
|
||||
"history.picking.busy.master": "Loading picking-list history...",
|
||||
"history.picking.busy.detail": "Loading picking-list detail...",
|
||||
"history.picking.msg.title": "Picking List History",
|
||||
"history.picking.msg.load_error": "Load failed:\n{error}",
|
||||
"history.picking.msg.detail_error": "Detail load failed:\n{error}",
|
||||
"reset.title": "Aisle Management - empty cells by aisle",
|
||||
"reset.label.aisle": "Aisle:",
|
||||
"reset.button.refresh": "Load",
|
||||
|
||||
121
main.py
121
main.py
@@ -11,6 +11,10 @@ import sys
|
||||
import tkinter as tk
|
||||
import time
|
||||
|
||||
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||
|
||||
ensure_stdio("warehouse_main")
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
|
||||
@@ -24,6 +28,8 @@ from login_window import prompt_login
|
||||
from locale_text import load_locale_catalog, text as loc_text
|
||||
from reset_corsie import open_reset_corsie_window
|
||||
from search_pallets import open_search_window
|
||||
from storico_pickinglist import open_storico_pickinglist_window
|
||||
from storico_udc import open_storico_udc_window
|
||||
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
|
||||
from ui_theme import theme_font, theme_section, theme_value
|
||||
from user_session import UserSession, create_user_session
|
||||
@@ -45,9 +51,9 @@ BYPASS_LOGIN_USER = {
|
||||
"codice_unita": "U1",
|
||||
}
|
||||
|
||||
# Create one global loop and make it the default everywhere.
|
||||
# Create one global loop for database work. Tk must keep the main thread clean;
|
||||
# callers schedule async jobs on this loop explicitly.
|
||||
_loop = get_global_loop()
|
||||
asyncio.set_event_loop(_loop)
|
||||
|
||||
|
||||
def _noop(*args, **kwargs):
|
||||
@@ -148,7 +154,9 @@ class Launcher(ctk.CTk):
|
||||
"layout",
|
||||
"multi_udc",
|
||||
"search",
|
||||
"storico_udc",
|
||||
"pickinglist",
|
||||
"storico_pickinglist",
|
||||
]
|
||||
|
||||
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
|
||||
@@ -164,6 +172,9 @@ class Launcher(ctk.CTk):
|
||||
self._is_cascading = False
|
||||
self._focus_restore_pending: set[str] = set()
|
||||
self._restore_suppressed_until = 0.0
|
||||
self._exit_icon = self._make_exit_icon(
|
||||
color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d"))
|
||||
)
|
||||
self.title(
|
||||
f"{loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse 1.0.0')} - {self.session.display_name}"
|
||||
)
|
||||
@@ -185,45 +196,63 @@ class Launcher(ctk.CTk):
|
||||
"reset_corsie",
|
||||
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
|
||||
"launcher.open_reset_corsie",
|
||||
lambda: self._open_child_window(
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"reset_corsie",
|
||||
open_reset_corsie_window(self, self.db_client, session=self.session),
|
||||
lambda: open_reset_corsie_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"layout",
|
||||
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
|
||||
"launcher.open_layout",
|
||||
lambda: self._open_child_window(
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"layout",
|
||||
open_layout_window(self, self.db_client, session=self.session),
|
||||
lambda: open_layout_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"multi_udc",
|
||||
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
|
||||
"launcher.open_multi_udc",
|
||||
lambda: self._open_child_window(
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"multi_udc",
|
||||
open_celle_multiple_window(self, self.db_client, session=self.session),
|
||||
lambda: open_celle_multiple_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"search",
|
||||
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
|
||||
"launcher.open_search",
|
||||
lambda: self._open_child_window(
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"search",
|
||||
open_search_window(self, self.db_client, session=self.session),
|
||||
lambda: open_search_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"storico_udc",
|
||||
loc_text("launcher.history_udc", catalog=self._locale_catalog, default="Storico movimenti UDC"),
|
||||
"launcher.open_history_udc",
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"storico_udc",
|
||||
lambda: open_storico_udc_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"pickinglist",
|
||||
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
|
||||
"launcher.open_pickinglist",
|
||||
lambda: self._open_child_window(
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"pickinglist",
|
||||
open_pickinglist_window(self, self.db_client, session=self.session),
|
||||
lambda: open_pickinglist_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"storico_pickinglist",
|
||||
loc_text("launcher.history_pickinglist", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||
"launcher.open_history_pickinglist",
|
||||
lambda: self._open_or_focus_child_window(
|
||||
"storico_pickinglist",
|
||||
lambda: open_storico_pickinglist_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
@@ -257,12 +286,23 @@ class Launcher(ctk.CTk):
|
||||
for idx, (_key, label, permission, callback) in enumerate(actions):
|
||||
row = 1 + (idx // max_buttons_per_row)
|
||||
column = idx % max_buttons_per_row
|
||||
text = label
|
||||
button_options = {}
|
||||
if _key == "exit":
|
||||
row = 2
|
||||
column = max_buttons_per_row - 1
|
||||
text = label
|
||||
button_options = {
|
||||
"image": self._exit_icon,
|
||||
"compound": "left",
|
||||
}
|
||||
button = ctk.CTkButton(
|
||||
wrap,
|
||||
text=label,
|
||||
text=text,
|
||||
font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
|
||||
state="normal" if self.session.can(permission) else "disabled",
|
||||
command=callback,
|
||||
**button_options,
|
||||
)
|
||||
button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
|
||||
tip = tooltip_text(permission, catalog=self._tooltip_catalog)
|
||||
@@ -282,6 +322,18 @@ class Launcher(ctk.CTk):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _make_exit_icon(self, *, color: str) -> tk.PhotoImage:
|
||||
"""Create a small red X icon without adding image assets to the project."""
|
||||
|
||||
size = 14
|
||||
image = tk.PhotoImage(width=size, height=size)
|
||||
for offset in range(3, 11):
|
||||
image.put(color, (offset, offset))
|
||||
image.put(color, (offset + 1, offset))
|
||||
image.put(color, (offset, size - 1 - offset))
|
||||
image.put(color, (offset + 1, size - 1 - offset))
|
||||
return image
|
||||
|
||||
def _apply_dynamic_geometry(self) -> None:
|
||||
"""Size the launcher around its current content and keep it docked at the top."""
|
||||
|
||||
@@ -334,6 +386,34 @@ class Launcher(ctk.CTk):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _open_or_focus_child_window(self, key: str, factory) -> None:
|
||||
"""Open one child per launcher key, or focus the existing window."""
|
||||
|
||||
self._child_windows_by_key = {
|
||||
child_key: child
|
||||
for child_key, child in self._child_windows_by_key.items()
|
||||
if getattr(child, "winfo_exists", lambda: False)()
|
||||
}
|
||||
existing = self._child_windows_by_key.get(key)
|
||||
if existing is not None and getattr(existing, "winfo_exists", lambda: False)():
|
||||
try:
|
||||
if hasattr(existing, "state") and existing.state() == "iconic":
|
||||
existing.deiconify()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
existing.lift()
|
||||
existing.focus_force()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
place_window_below_parent_later(self, existing)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
self._open_child_window(key, factory())
|
||||
|
||||
def _forget_child_window(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None:
|
||||
"""Remove stale references to child windows that have been closed."""
|
||||
|
||||
@@ -460,7 +540,9 @@ class Launcher(ctk.CTk):
|
||||
self.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def run_app() -> int:
|
||||
"""Run the backoffice application entry point."""
|
||||
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_default_color_theme("green")
|
||||
if not _acquire_single_instance_mutex():
|
||||
@@ -479,7 +561,7 @@ if __name__ == "__main__":
|
||||
stop_global_loop()
|
||||
except Exception:
|
||||
pass
|
||||
raise SystemExit(0)
|
||||
return 0
|
||||
|
||||
db_cfg = ensure_db_config(_loop)
|
||||
if db_cfg is None:
|
||||
@@ -487,7 +569,7 @@ if __name__ == "__main__":
|
||||
stop_global_loop()
|
||||
except Exception:
|
||||
pass
|
||||
raise SystemExit(0)
|
||||
return 0
|
||||
|
||||
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
|
||||
|
||||
@@ -511,7 +593,7 @@ if __name__ == "__main__":
|
||||
|
||||
if session is None:
|
||||
_shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
|
||||
raise SystemExit(0)
|
||||
return 0
|
||||
|
||||
_destroy_tk_root(bootstrap)
|
||||
|
||||
@@ -519,3 +601,8 @@ if __name__ == "__main__":
|
||||
Launcher(session, db_app).mainloop()
|
||||
finally:
|
||||
_shutdown_runtime(bootstrap=None, dispose_db=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))
|
||||
|
||||
@@ -255,7 +255,7 @@ async def _execute(db, sql: str, params: Dict[str, Any]) -> int:
|
||||
|
||||
@_log_call()
|
||||
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult:
|
||||
"""Execute the original reservation stored procedure used by the C# client."""
|
||||
"""Execute the Python-specific picking-list reservation stored procedure."""
|
||||
try:
|
||||
azione = str(Azione or "P").strip().upper()
|
||||
if azione not in ("P", "S"):
|
||||
@@ -268,7 +268,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
||||
SET NOCOUNT ON;
|
||||
DECLARE @RC int = 0;
|
||||
|
||||
EXEC dbo.sp_xExePackingListPallet
|
||||
EXEC dbo.py_sp_xExePackingListPallet
|
||||
@IDOperatore = :IDOperatore,
|
||||
@Documento = :Documento,
|
||||
@Azione = :Azione,
|
||||
@@ -276,7 +276,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
||||
|
||||
SELECT CAST(@RC AS int) AS RC;
|
||||
"""
|
||||
_log_sql("sp_xExePackingListPallet", sql, {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione})
|
||||
_log_sql("py_sp_xExePackingListPallet", sql, {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione})
|
||||
if not hasattr(db, "query_json"):
|
||||
raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.")
|
||||
res = await db.query_json(
|
||||
@@ -288,7 +288,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
||||
rows = []
|
||||
if isinstance(res, dict):
|
||||
rows = res.get("rows", []) or []
|
||||
_log_dataset("sp_xExePackingListPallet", rows)
|
||||
_log_dataset("py_sp_xExePackingListPallet", rows)
|
||||
rc = 0
|
||||
if rows and isinstance(rows[0], dict):
|
||||
try:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
aioodbc
|
||||
customtkinter
|
||||
aioodbc==0.5.0
|
||||
customtkinter==5.2.2
|
||||
loguru
|
||||
openpyxl
|
||||
openpyxl==3.1.5
|
||||
orjson
|
||||
pyodbc
|
||||
SQLAlchemy
|
||||
tksheet
|
||||
pyodbc==5.3.0
|
||||
SQLAlchemy==2.0.49
|
||||
tksheet==7.6.0
|
||||
|
||||
24
rollback_online_history_forms_patch.sql
Normal file
24
rollback_online_history_forms_patch.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Rollback online - form storiche Python
|
||||
|
||||
Rimuove solo le viste Python-only usate dalla form "Storico Picking List".
|
||||
Non tocca oggetti C# legacy e non tocca dati.
|
||||
|
||||
Nota:
|
||||
- la form "Storico movimenti UDC" non ha oggetti dedicati da rimuovere.
|
||||
*/
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingListStorico', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_XMag_ViewPackingListStorico;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_vPreparaPackingList', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_vPreparaPackingList;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_vPreparaPackingListSAMA1', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_vPreparaPackingListSAMA1;
|
||||
GO
|
||||
|
||||
PRINT 'Rollback form storiche Python completato.';
|
||||
GO
|
||||
35
rollback_python_parallel_pickinglist_patch.sql
Normal file
35
rollback_python_parallel_pickinglist_patch.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
|
||||
/*
|
||||
Rollback della patch parallela Python.
|
||||
|
||||
Non ripristina oggetti legacy, perche' la patch parallela non li modifica.
|
||||
Rimuove solo gli oggetti dbo.py_* e la tabella di prenotazione Python.
|
||||
*/
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_sp_ControllaPrenotazionePackingListPalletNew', N'P') IS NOT NULL
|
||||
DROP PROCEDURE dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_sp_xExePackingListPalletPrenota', N'P') IS NOT NULL
|
||||
DROP PROCEDURE dbo.py_sp_xExePackingListPalletPrenota;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_sp_xExePackingListPallet', N'P') IS NOT NULL
|
||||
DROP PROCEDURE dbo.py_sp_xExePackingListPallet;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_ViewPackingListRestante', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_ViewPackingListRestante;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingList', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_XMag_ViewPackingList;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NOT NULL
|
||||
DROP TABLE dbo.PyPickingListReservation;
|
||||
GO
|
||||
20
rollback_python_pickinglist_history_views.sql
Normal file
20
rollback_python_pickinglist_history_views.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Rollback SQL - rimozione viste storiche picking list Python.
|
||||
|
||||
Non tocca oggetti C# legacy.
|
||||
*/
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingListStorico', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_XMag_ViewPackingListStorico;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_vPreparaPackingList', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_vPreparaPackingList;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.py_vPreparaPackingListSAMA1', N'V') IS NOT NULL
|
||||
DROP VIEW dbo.py_vPreparaPackingListSAMA1;
|
||||
GO
|
||||
|
||||
PRINT 'Viste storiche Python rimosse.';
|
||||
GO
|
||||
68
runtime_support.py
Normal file
68
runtime_support.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Runtime helpers for console-less Windows launches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
FATAL_LOG = BASE_DIR / "warehouse_fatal.log"
|
||||
_STDIO_HANDLES = []
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def ensure_stdio(app_name: str) -> None:
|
||||
"""Give ``pythonw`` a real stdout/stderr target before loggers are imported."""
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d")
|
||||
if sys.stdout is None:
|
||||
handle = (BASE_DIR / f"{app_name}_stdout_{stamp}.log").open("a", encoding="utf-8", buffering=1)
|
||||
_STDIO_HANDLES.append(handle)
|
||||
sys.stdout = handle
|
||||
if sys.stderr is None:
|
||||
handle = (BASE_DIR / f"{app_name}_stderr_{stamp}.log").open("a", encoding="utf-8", buffering=1)
|
||||
_STDIO_HANDLES.append(handle)
|
||||
sys.stderr = handle
|
||||
|
||||
|
||||
def log_fatal(app_name: str, exc: BaseException) -> None:
|
||||
"""Write one startup/runtime crash to a persistent log file."""
|
||||
|
||||
with FATAL_LOG.open("a", encoding="utf-8") as handle:
|
||||
handle.write("\n" + "=" * 80 + "\n")
|
||||
handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}\n")
|
||||
handle.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
||||
|
||||
|
||||
def show_fatal_message(app_name: str, exc: BaseException) -> None:
|
||||
"""Show a best-effort message box for console-less launches."""
|
||||
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror(
|
||||
app_name,
|
||||
f"Avvio non riuscito.\n\nDettaglio salvato in:\n{FATAL_LOG}\n\n{exc}",
|
||||
parent=root,
|
||||
)
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_with_fatal_log(app_name: str, func: Callable[[], T]) -> T:
|
||||
"""Run an app entry point and persist otherwise invisible ``pythonw`` crashes."""
|
||||
|
||||
try:
|
||||
return func()
|
||||
except BaseException as exc:
|
||||
log_fatal(app_name, exc)
|
||||
show_fatal_message(app_name, exc)
|
||||
raise
|
||||
68
specifica_storico_pickinglist.md
Normal file
68
specifica_storico_pickinglist.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Specifica - Storico Picking List
|
||||
|
||||
## Obiettivo
|
||||
|
||||
La finestra "Storico Picking List" deve permettere di consultare le picking list visibili nel modello dati Python parallelo, comprese quelle gia' esaurite, mostrando lo stato sintetico della lista e il dettaglio delle UDC.
|
||||
|
||||
## Accesso
|
||||
|
||||
- La finestra si apre dal launcher principale tramite il pulsante "Storico Picking List".
|
||||
- In una fase successiva potra' essere collegata alla form "Gestione Picking List" per aprire direttamente il dettaglio della lista selezionata.
|
||||
- La finestra e' disponibile agli operatori autenticati secondo il permesso `launcher.open_history_pickinglist`.
|
||||
|
||||
## Comportamento UI
|
||||
|
||||
- La finestra usa lo stesso posizionamento delle altre form del backoffice.
|
||||
- Tutte le interrogazioni al database sono asincrone.
|
||||
- Durante il caricamento della lista e del dettaglio deve comparire l'overlay standard dell'applicazione.
|
||||
- La parte alta mostra l'elenco delle picking list.
|
||||
- La parte bassa mostra il dettaglio della picking list selezionata.
|
||||
- Il filtro principale e' il numero documento, con ricerca parziale.
|
||||
|
||||
## Logica dati
|
||||
|
||||
La form usa un ramo di lettura storico Python separato dalle viste operative C#.
|
||||
|
||||
Oggetti SQL dedicati:
|
||||
|
||||
- `dbo.py_vPreparaPackingListSAMA1`
|
||||
- `dbo.py_vPreparaPackingList`
|
||||
- `dbo.py_XMag_ViewPackingListStorico`
|
||||
|
||||
Questi oggetti sono creati dallo script `apply_python_pickinglist_history_views.sql` e possono essere rimossi con `rollback_python_pickinglist_history_views.sql`.
|
||||
|
||||
La vista storica non usa `dbo.vPreparaPackingListSAMA1`, `dbo.vPreparaPackingList` o `dbo.XMag_ViewPackingList`, cosi' il programma C# continua a lavorare sul proprio ramo legacy.
|
||||
|
||||
La vista `dbo.py_vPreparaPackingListSAMA1` include:
|
||||
|
||||
- documenti dell'anno corrente: `BAMTES.ANNDOC >= YEAR(GETDATE())`
|
||||
- documenti aperti e chiusi: `BAMTES.STATO IN ('P', 'D')`
|
||||
- nessun filtro sugli ultimi 10 giorni
|
||||
|
||||
La lista alta aggrega per documento:
|
||||
|
||||
- data documento
|
||||
- stato documento gestionale
|
||||
- totale UDC
|
||||
- righe residue
|
||||
- righe spedite
|
||||
- stato operativo
|
||||
- stato prenotazione Python
|
||||
|
||||
Lo stato operativo e' calcolato cosi':
|
||||
|
||||
- `Chiusa`: il documento gestionale ha `BAMTES.STATO = 'D'`.
|
||||
- `Da lavorare`: nessuna riga risulta spedita.
|
||||
- `In corso`: almeno una riga risulta spedita e almeno una riga resta da lavorare.
|
||||
- `Esaurita`: non restano righe operative.
|
||||
|
||||
Il criterio primario per riconoscere una picking list lavorata nello storico e' lo stato gestionale `BAMTES.STATO`.
|
||||
La cella `9999 = 7G.1.1` resta un indizio logistico importante, ma non e' sufficiente come unico criterio storico perche' una UDC non piu' in giacenza puo' ricadere nei fallback della vista.
|
||||
|
||||
## Convenzioni operative
|
||||
|
||||
Una riga con `Cella = 9999` viene considerata spedita/prelevata, coerentemente con la convenzione `7G.1.1` osservata sul barcode.
|
||||
|
||||
## Limiti noti
|
||||
|
||||
Questa finestra e' una prima vista storica di consultazione. Non sostituisce ancora una vera storicizzazione annuale progettata a database. Quando il database verra' riprogettato per FlyWMS, sara' preferibile costruire una vista storica dedicata, con date di chiusura lista, utente, tempi e movimento UDC collegato.
|
||||
53
specifica_storico_udc.md
Normal file
53
specifica_storico_udc.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Specifica - Storico movimenti UDC
|
||||
|
||||
## Obiettivo
|
||||
|
||||
La finestra "Storico movimenti UDC" deve permettere di ricostruire, in sola lettura, la sequenza dei movimenti registrati per una UDC. La funzione nasce come strumento di diagnosi per capire dove e da chi una unita' di carico e' stata movimentata fino alla spedizione.
|
||||
|
||||
## Accesso
|
||||
|
||||
- La finestra si apre dal launcher principale tramite il pulsante "Storico movimenti UDC".
|
||||
- In una fase successiva potra' essere richiamata anche dalle form esistenti, ad esempio dalla ricerca UDC o dal dettaglio picking list, passando direttamente il codice UDC selezionato.
|
||||
- La finestra e' disponibile agli operatori autenticati secondo il permesso `launcher.open_history_udc`.
|
||||
|
||||
## Comportamento UI
|
||||
|
||||
- La finestra usa lo stesso posizionamento delle altre form del backoffice.
|
||||
- Tutte le interrogazioni al database sono asincrone.
|
||||
- Durante il caricamento deve comparire l'overlay standard dell'applicazione.
|
||||
- Il filtro principale e' il codice UDC, con ricerca parziale.
|
||||
- La griglia mostra al massimo 500 righe per evitare carichi eccessivi durante i test.
|
||||
- Le righe di tipo `V` sono evidenziate come versamenti/carichi.
|
||||
- Le righe di tipo `P` sono evidenziate come prelievi/scarichi.
|
||||
|
||||
## Dati visualizzati
|
||||
|
||||
La prima implementazione legge da `dbo.MagazziniPallet`, collegando quando possibile `dbo.Celle` per mostrare l'ubicazione in forma leggibile.
|
||||
|
||||
La ricerca non si limita al match diretto su `MagazziniPallet.Attributo`: include anche i movimenti collegati tramite la catena `ID` / `IDRiferimento`. Questo e' importante nel vecchio modello dati, perche' un prelievo o un trasferimento puo' essere legato alla riga originaria di versamento.
|
||||
|
||||
La griglia integra anche la vista `dbo.XMag_GiacenzaPalletPlistChiuse` come fallback gestionale:
|
||||
|
||||
- prima vengono mostrati i movimenti fisici reali `V` e `P` da `dbo.MagazziniPallet`
|
||||
- se per la UDC esiste almeno un movimento fisico `P`, anche collegato tramite la catena `ID` / `IDRiferimento`, non viene aggiunta nessuna riga diagnostica
|
||||
- se non esistono movimenti `P` ma la UDC compare in una picking list gestionale chiusa, viene mostrata una riga diagnostica `SPED`
|
||||
|
||||
La riga `SPED` non e' un movimento fisico: serve solo a spiegare perche' le form operative considerano la UDC gia' spedita anche quando manca la tracciatura fisica `P`. Deve quindi essere considerata l'ultima opzione: se esistono movimenti reali `V` o `P`, questi restano la fonte primaria per data, utente e ubicazione.
|
||||
|
||||
Campi principali:
|
||||
|
||||
- ID movimento
|
||||
- Tipo movimento
|
||||
- Riferimento
|
||||
- UDC
|
||||
- IDCella
|
||||
- Ubicazione
|
||||
- DataMagazzino
|
||||
- Utente di inserimento
|
||||
- Data inserimento
|
||||
- Utente di modifica
|
||||
- Data modifica
|
||||
|
||||
## Limiti noti
|
||||
|
||||
La funzione e' diagnostica e non modifica dati. Se in futuro servira' uno storico piu' ricco, sara' opportuno introdurre una vista o tabella storica dedicata con nomi parlanti nel nuovo schema FlyWMS.
|
||||
364
storico_pickinglist.py
Normal file
364
storico_pickinglist.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""Read-only picking-list history window."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
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, theme_value
|
||||
from window_placement import place_window_fullsize_below_parent_later
|
||||
|
||||
|
||||
SQL_STORICO_PL = """
|
||||
WITH base AS (
|
||||
SELECT *
|
||||
FROM dbo.py_XMag_ViewPackingListStorico
|
||||
),
|
||||
agg AS (
|
||||
SELECT
|
||||
Documento,
|
||||
MAX(DataDocumento) AS DataDocumento,
|
||||
MAX(StatoDocumento) AS StatoDocumento,
|
||||
MAX(NAZIONE) AS NAZIONE,
|
||||
MAX(CodNazione) AS CodNazione,
|
||||
COUNT(DISTINCT Pallet) AS TotUDC,
|
||||
SUM(CASE WHEN Cella = 9999 THEN 1 ELSE 0 END) AS RigheSpedite,
|
||||
SUM(CASE WHEN Cella <> 9999 OR Cella IS NULL THEN 1 ELSE 0 END) AS RigheResidue,
|
||||
COUNT(*) AS RigheTotali,
|
||||
MIN(Ordinamento) AS PrimoOrdine,
|
||||
MAX(IDStato) AS IDStato
|
||||
FROM base
|
||||
GROUP BY Documento
|
||||
)
|
||||
SELECT
|
||||
Documento,
|
||||
DataDocumento,
|
||||
StatoDocumento,
|
||||
NAZIONE,
|
||||
CodNazione,
|
||||
TotUDC,
|
||||
RigheResidue,
|
||||
RigheSpedite,
|
||||
RigheTotali,
|
||||
CASE
|
||||
WHEN StatoDocumento = 'D' THEN 'Chiusa'
|
||||
WHEN RigheResidue = 0 THEN 'Esaurita'
|
||||
WHEN RigheSpedite > 0 THEN 'In corso'
|
||||
ELSE 'Da lavorare'
|
||||
END AS StatoOperativo,
|
||||
IDStato,
|
||||
PrimoOrdine
|
||||
FROM agg
|
||||
WHERE (:documento IS NULL OR CAST(Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
|
||||
ORDER BY Documento DESC;
|
||||
"""
|
||||
|
||||
SQL_STORICO_PL_DETAILS = """
|
||||
SELECT
|
||||
Documento,
|
||||
Pallet,
|
||||
Lotto,
|
||||
Articolo,
|
||||
Descrizione,
|
||||
Qta,
|
||||
DataDocumento,
|
||||
StatoDocumento,
|
||||
Cella,
|
||||
Ubicazione,
|
||||
Ordinamento,
|
||||
IDStato
|
||||
FROM dbo.py_XMag_ViewPackingListStorico
|
||||
WHERE Documento = :documento
|
||||
ORDER BY Ordinamento, Pallet;
|
||||
"""
|
||||
|
||||
|
||||
def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
if not isinstance(res, dict):
|
||||
return []
|
||||
rows = res.get("rows") or []
|
||||
cols = res.get("columns") or []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
return [row for row in rows if isinstance(row, dict)]
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if isinstance(row, (list, tuple)) and cols:
|
||||
out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))})
|
||||
return out
|
||||
|
||||
|
||||
def _format_date(value: Any) -> str:
|
||||
"""Format SQL Server/SAMA date values into dd/mm/yyyy for operators."""
|
||||
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime("%d/%m/%Y")
|
||||
if isinstance(value, date):
|
||||
return value.strftime("%d/%m/%Y")
|
||||
if isinstance(value, (int, float)):
|
||||
try:
|
||||
# SQL Server numeric datetime: CAST(datetime AS int), day 0 = 1900-01-01.
|
||||
return (datetime(1900, 1, 1) + timedelta(days=int(value))).strftime("%d/%m/%Y")
|
||||
except Exception:
|
||||
return str(value)
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return ""
|
||||
if text.isdigit() and len(text) == 8:
|
||||
try:
|
||||
return datetime.strptime(text, "%Y%m%d").strftime("%d/%m/%Y")
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return datetime.fromisoformat(text.replace("Z", "+00:00")).strftime("%d/%m/%Y")
|
||||
except Exception:
|
||||
return text
|
||||
|
||||
|
||||
class StoricoPickingListWindow(ctk.CTkToplevel):
|
||||
"""Window that shows historical picking-list status and details."""
|
||||
|
||||
def __init__(self, parent: tk.Widget, db_client, session=None):
|
||||
super().__init__(parent)
|
||||
self.db_client = db_client
|
||||
self.session = session
|
||||
self._theme = theme_section("history_picking_window", theme_section("pickinglist_window", {}))
|
||||
self._locale_catalog = load_locale_catalog()
|
||||
self._async = AsyncRunner(self)
|
||||
self._busy = InlineBusyOverlay(self, self._theme)
|
||||
self.var_documento = tk.StringVar()
|
||||
|
||||
self.title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"))
|
||||
self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720")))
|
||||
minsize = theme_value(self._theme, "window_minsize", [980, 560])
|
||||
self.minsize(int(minsize[0]), int(minsize[1]))
|
||||
try:
|
||||
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._build_ui()
|
||||
self.after(300, self._load_master)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(3, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
top = ctk.CTkFrame(
|
||||
self,
|
||||
fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")),
|
||||
)
|
||||
top.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
|
||||
top.grid_columnconfigure(3, weight=1)
|
||||
|
||||
label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10))
|
||||
entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10))
|
||||
button_font = theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))
|
||||
|
||||
ctk.CTkLabel(top, text="Documento:", font=label_font).grid(row=0, column=0, sticky="w")
|
||||
ctk.CTkEntry(top, textvariable=self.var_documento, width=140, font=entry_font).grid(
|
||||
row=0, column=1, sticky="w", padx=(4, 12)
|
||||
)
|
||||
ctk.CTkButton(
|
||||
top,
|
||||
text=loc_text("history.picking.button.reload", catalog=self._locale_catalog, default="Ricarica"),
|
||||
command=self._load_master,
|
||||
font=button_font,
|
||||
).grid(
|
||||
row=0, column=2, sticky="w"
|
||||
)
|
||||
|
||||
self.master_tree = self._make_tree(
|
||||
row=1,
|
||||
columns=("Documento", "Data", "StatoDoc", "NAZIONE", "TotUDC", "Residue", "Spedite", "Stato", "IDStato"),
|
||||
widths={
|
||||
"Documento": 100,
|
||||
"Data": 110,
|
||||
"StatoDoc": 75,
|
||||
"NAZIONE": 260,
|
||||
"TotUDC": 90,
|
||||
"Residue": 90,
|
||||
"Spedite": 90,
|
||||
"Stato": 120,
|
||||
"IDStato": 80,
|
||||
},
|
||||
)
|
||||
self.master_tree.bind("<<TreeviewSelect>>", self._on_master_select)
|
||||
|
||||
ctk.CTkLabel(
|
||||
self,
|
||||
text=loc_text("history.picking.detail_title", catalog=self._locale_catalog, default="Dettaglio contenuto"),
|
||||
anchor="w",
|
||||
font=button_font,
|
||||
).grid(
|
||||
row=2, column=0, sticky="ew", padx=8, pady=(4, 2)
|
||||
)
|
||||
self.detail_tree = self._make_tree(
|
||||
row=3,
|
||||
columns=("Pallet", "Lotto", "Articolo", "Descrizione", "Qta", "Data", "StatoDoc", "Cella", "Ubicazione", "Ordine"),
|
||||
widths={
|
||||
"Pallet": 120,
|
||||
"Lotto": 120,
|
||||
"Articolo": 130,
|
||||
"Descrizione": 320,
|
||||
"Qta": 80,
|
||||
"Data": 110,
|
||||
"StatoDoc": 75,
|
||||
"Cella": 80,
|
||||
"Ubicazione": 180,
|
||||
"Ordine": 80,
|
||||
},
|
||||
)
|
||||
|
||||
def _make_tree(self, *, row: int, columns: tuple[str, ...], widths: dict[str, int]) -> ttk.Treeview:
|
||||
wrap = ctk.CTkFrame(self)
|
||||
wrap.grid(row=row, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
||||
wrap.grid_rowconfigure(0, weight=1)
|
||||
wrap.grid_columnconfigure(0, weight=1)
|
||||
tree = ttk.Treeview(wrap, columns=columns, show="headings")
|
||||
for col in columns:
|
||||
tree.heading(col, text=col)
|
||||
tree.column(col, width=widths.get(col, 100), anchor="w")
|
||||
tree.tag_configure("done", background="#ECECEC")
|
||||
tree.tag_configure("active", background="#EAF7EA")
|
||||
sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview)
|
||||
sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview)
|
||||
tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
||||
tree.grid(row=0, column=0, sticky="nsew")
|
||||
sy.grid(row=0, column=1, sticky="ns")
|
||||
sx.grid(row=1, column=0, sticky="ew")
|
||||
return tree
|
||||
|
||||
def _load_master(self) -> None:
|
||||
params = {"documento": str(self.var_documento.get() or "").strip() or None}
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_STORICO_PL, params)
|
||||
|
||||
def _ok(res):
|
||||
self._fill_master(_rows_to_dicts(res))
|
||||
|
||||
def _err(ex):
|
||||
messagebox.showerror(
|
||||
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||
loc_text(
|
||||
"history.picking.msg.load_error",
|
||||
catalog=self._locale_catalog,
|
||||
default="Errore caricamento:\n{error}",
|
||||
).format(error=ex),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self._async.run(
|
||||
_job(),
|
||||
_ok,
|
||||
_err,
|
||||
busy=self._busy,
|
||||
message=loc_text(
|
||||
"history.picking.busy.master",
|
||||
catalog=self._locale_catalog,
|
||||
default="Carico storico picking list...",
|
||||
),
|
||||
)
|
||||
|
||||
def _fill_master(self, rows: list[dict[str, Any]]) -> None:
|
||||
self.master_tree.delete(*self.master_tree.get_children(""))
|
||||
self.detail_tree.delete(*self.detail_tree.get_children(""))
|
||||
for index, row in enumerate(rows):
|
||||
stato = str(row.get("StatoOperativo") or "")
|
||||
tag = "done" if stato in {"Chiusa", "Esaurita"} else "active" if int(row.get("IDStato") or 0) == 1 else ""
|
||||
self.master_tree.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=f"doc_{row.get('Documento')}_{index}",
|
||||
values=(
|
||||
row.get("Documento", ""),
|
||||
_format_date(row.get("DataDocumento", "")),
|
||||
row.get("StatoDocumento", ""),
|
||||
row.get("NAZIONE", ""),
|
||||
row.get("TotUDC", ""),
|
||||
row.get("RigheResidue", ""),
|
||||
row.get("RigheSpedite", ""),
|
||||
stato,
|
||||
row.get("IDStato", ""),
|
||||
),
|
||||
tags=(tag,) if tag else (),
|
||||
)
|
||||
|
||||
def _on_master_select(self, _event=None) -> None:
|
||||
selected = self.master_tree.selection()
|
||||
if not selected:
|
||||
return
|
||||
values = self.master_tree.item(selected[0], "values")
|
||||
if not values:
|
||||
return
|
||||
documento = values[0]
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento})
|
||||
|
||||
def _ok(res):
|
||||
self._fill_detail(_rows_to_dicts(res))
|
||||
|
||||
def _err(ex):
|
||||
messagebox.showerror(
|
||||
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||
loc_text(
|
||||
"history.picking.msg.detail_error",
|
||||
catalog=self._locale_catalog,
|
||||
default="Errore dettaglio:\n{error}",
|
||||
).format(error=ex),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self._async.run(
|
||||
_job(),
|
||||
_ok,
|
||||
_err,
|
||||
busy=self._busy,
|
||||
message=loc_text(
|
||||
"history.picking.busy.detail",
|
||||
catalog=self._locale_catalog,
|
||||
default="Carico dettaglio picking list...",
|
||||
),
|
||||
)
|
||||
|
||||
def _fill_detail(self, rows: list[dict[str, Any]]) -> None:
|
||||
self.detail_tree.delete(*self.detail_tree.get_children(""))
|
||||
for row in rows:
|
||||
done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999
|
||||
self.detail_tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
row.get("Pallet", ""),
|
||||
row.get("Lotto", ""),
|
||||
row.get("Articolo", ""),
|
||||
row.get("Descrizione", ""),
|
||||
row.get("Qta", ""),
|
||||
_format_date(row.get("DataDocumento", "")),
|
||||
row.get("StatoDocumento", ""),
|
||||
row.get("Cella", ""),
|
||||
row.get("Ubicazione", ""),
|
||||
row.get("Ordinamento", ""),
|
||||
),
|
||||
tags=("done",) if done else (),
|
||||
)
|
||||
|
||||
|
||||
def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc:
|
||||
"""Open the picking-list history window."""
|
||||
|
||||
win = StoricoPickingListWindow(parent, db_client, session=session)
|
||||
place_window_fullsize_below_parent_later(parent, win)
|
||||
win.bind("<Escape>", lambda _e: win.destroy())
|
||||
return win
|
||||
312
storico_udc.py
Normal file
312
storico_udc.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""Read-only UDC movement history window."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
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, theme_value
|
||||
from window_placement import place_window_fullsize_below_parent_later
|
||||
|
||||
|
||||
SQL_STORICO_UDC = """
|
||||
WITH direct AS (
|
||||
SELECT
|
||||
ID,
|
||||
IDRiferimento
|
||||
FROM dbo.MagazziniPallet
|
||||
WHERE (:udc IS NULL OR Attributo COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%'))
|
||||
),
|
||||
shipped AS (
|
||||
SELECT
|
||||
shipped.BarcodePallet,
|
||||
shipped.NumeroPallet,
|
||||
shipped.IDMagazzino,
|
||||
shipped.IDArea,
|
||||
shipped.IDCella
|
||||
FROM dbo.XMag_GiacenzaPalletPlistChiuse shipped
|
||||
WHERE (:udc IS NULL OR shipped.BarcodePallet COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%'))
|
||||
),
|
||||
has_physical_p AS (
|
||||
SELECT DISTINCT
|
||||
s.BarcodePallet
|
||||
FROM shipped s
|
||||
JOIN dbo.MagazziniPallet seed
|
||||
ON seed.Attributo COLLATE Latin1_General_CI_AS =
|
||||
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
JOIN dbo.MagazziniPallet mp
|
||||
ON mp.Tipo = 'P'
|
||||
AND (
|
||||
mp.Attributo COLLATE Latin1_General_CI_AS =
|
||||
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
OR mp.ID = seed.ID
|
||||
OR mp.IDRiferimento = seed.ID
|
||||
OR (seed.IDRiferimento IS NOT NULL AND seed.IDRiferimento > 0 AND mp.ID = seed.IDRiferimento)
|
||||
OR (seed.IDRiferimento IS NOT NULL AND seed.IDRiferimento > 0 AND mp.IDRiferimento = seed.IDRiferimento)
|
||||
)
|
||||
),
|
||||
roots AS (
|
||||
SELECT ID AS RootID
|
||||
FROM direct
|
||||
UNION
|
||||
SELECT IDRiferimento AS RootID
|
||||
FROM direct
|
||||
WHERE IDRiferimento IS NOT NULL AND IDRiferimento > 0
|
||||
UNION
|
||||
SELECT mp.ID AS RootID
|
||||
FROM dbo.MagazziniPallet mp
|
||||
JOIN shipped s
|
||||
ON s.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||
)
|
||||
SELECT TOP (500) *
|
||||
FROM (
|
||||
SELECT
|
||||
CAST(mp.ID AS int) AS ID,
|
||||
CAST(mp.Tipo AS varchar(8)) AS Tipo,
|
||||
CAST(mp.IDRiferimento AS int) AS IDRiferimento,
|
||||
CAST(mp.NumeroPallet AS int) AS NumeroPallet,
|
||||
CAST(mp.Attributo AS varchar(16)) AS UDC,
|
||||
CAST(mp.IDMagazzino AS int) AS IDMagazzino,
|
||||
CAST(mp.IDArea AS int) AS IDArea,
|
||||
CAST(mp.IDCella AS int) AS IDCella,
|
||||
UPPER(
|
||||
CONCAT(
|
||||
COALESCE(LTRIM(RTRIM(c.Corsia)), 'NA'), '.',
|
||||
COALESCE(LTRIM(RTRIM(CAST(c.Colonna AS varchar(32)))), 'NA'), '.',
|
||||
COALESCE(LTRIM(RTRIM(CAST(c.Fila AS varchar(32)))), 'NA')
|
||||
)
|
||||
) AS Ubicazione,
|
||||
CAST(mp.DataMagazzino AS datetime) AS DataMagazzino,
|
||||
CAST(mp.InsUtente AS varchar(50)) AS InsUtente,
|
||||
CAST(mp.InsDataOra AS datetime) AS InsDataOra,
|
||||
CAST(mp.ModUtente AS varchar(50)) AS ModUtente,
|
||||
CAST(mp.ModDataOra AS datetime) AS ModDataOra
|
||||
FROM dbo.MagazziniPallet mp
|
||||
LEFT JOIN dbo.Celle c ON c.ID = mp.IDCella
|
||||
WHERE
|
||||
:udc IS NULL
|
||||
OR mp.Attributo COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%')
|
||||
OR mp.ID IN (SELECT RootID FROM roots)
|
||||
OR mp.IDRiferimento IN (SELECT RootID FROM roots)
|
||||
UNION ALL
|
||||
SELECT
|
||||
CAST(NULL AS int) AS ID,
|
||||
CAST('SPED' AS varchar(8)) AS Tipo,
|
||||
CAST(NULL AS int) AS IDRiferimento,
|
||||
CAST(s.NumeroPallet AS int) AS NumeroPallet,
|
||||
CAST(s.BarcodePallet AS varchar(16)) AS UDC,
|
||||
CAST(s.IDMagazzino AS int) AS IDMagazzino,
|
||||
CAST(s.IDArea AS int) AS IDArea,
|
||||
CAST(s.IDCella AS int) AS IDCella,
|
||||
UPPER(
|
||||
CONCAT(
|
||||
COALESCE(LTRIM(RTRIM(c.Corsia)), 'NA'), '.',
|
||||
COALESCE(LTRIM(RTRIM(CAST(c.Colonna AS varchar(32)))), 'NA'), '.',
|
||||
COALESCE(LTRIM(RTRIM(CAST(c.Fila AS varchar(32)))), 'NA')
|
||||
)
|
||||
) AS Ubicazione,
|
||||
CAST(NULL AS datetime) AS DataMagazzino,
|
||||
CAST('Picking list chiusa' AS varchar(50)) AS InsUtente,
|
||||
CAST(NULL AS datetime) AS InsDataOra,
|
||||
CAST(NULL AS varchar(50)) AS ModUtente,
|
||||
CAST(NULL AS datetime) AS ModDataOra
|
||||
FROM shipped s
|
||||
LEFT JOIN dbo.Celle c ON c.ID = s.IDCella
|
||||
LEFT JOIN has_physical_p hp
|
||||
ON hp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
WHERE hp.BarcodePallet IS NULL
|
||||
) rows
|
||||
ORDER BY UDC, CASE WHEN ID IS NULL THEN 0 ELSE 1 END DESC, ID DESC;
|
||||
"""
|
||||
|
||||
|
||||
def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
if not isinstance(res, dict):
|
||||
return []
|
||||
rows = res.get("rows") or []
|
||||
cols = res.get("columns") or []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
return [row for row in rows if isinstance(row, dict)]
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if isinstance(row, (list, tuple)) and cols:
|
||||
out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))})
|
||||
return out
|
||||
|
||||
|
||||
class StoricoUDCWindow(ctk.CTkToplevel):
|
||||
"""Window that shows the P/V movement timeline of one or more UDCs."""
|
||||
|
||||
def __init__(self, parent: tk.Widget, db_client, session=None, initial_udc: str | None = None):
|
||||
super().__init__(parent)
|
||||
self.db_client = db_client
|
||||
self.session = session
|
||||
self._theme = theme_section("history_udc_window", theme_section("search_window", {}))
|
||||
self._locale_catalog = load_locale_catalog()
|
||||
self._async = AsyncRunner(self)
|
||||
self._busy = InlineBusyOverlay(self, self._theme)
|
||||
self.var_udc = tk.StringVar(value=str(initial_udc or ""))
|
||||
|
||||
self.title(loc_text("history.udc.title", catalog=self._locale_catalog, default="Storico movimenti UDC"))
|
||||
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x720")))
|
||||
minsize = theme_value(self._theme, "window_minsize", [900, 560])
|
||||
self.minsize(int(minsize[0]), int(minsize[1]))
|
||||
try:
|
||||
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._build_ui()
|
||||
if initial_udc:
|
||||
self.after(250, self._do_search)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
top = ctk.CTkFrame(
|
||||
self,
|
||||
fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")),
|
||||
)
|
||||
top.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
|
||||
top.grid_columnconfigure(3, weight=1)
|
||||
|
||||
label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10))
|
||||
entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10))
|
||||
button_font = theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))
|
||||
|
||||
ctk.CTkLabel(top, text="UDC:", font=label_font).grid(row=0, column=0, sticky="w")
|
||||
ctk.CTkEntry(top, textvariable=self.var_udc, width=160, font=entry_font).grid(
|
||||
row=0, column=1, sticky="w", padx=(4, 12)
|
||||
)
|
||||
ctk.CTkButton(
|
||||
top,
|
||||
text=loc_text("history.udc.button.search", catalog=self._locale_catalog, default="Cerca"),
|
||||
command=self._do_search,
|
||||
font=button_font,
|
||||
).grid(
|
||||
row=0, column=2, sticky="w"
|
||||
)
|
||||
|
||||
wrap = ctk.CTkFrame(self)
|
||||
wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
||||
wrap.grid_rowconfigure(0, weight=1)
|
||||
wrap.grid_columnconfigure(0, weight=1)
|
||||
|
||||
cols = (
|
||||
"ID",
|
||||
"Tipo",
|
||||
"Rif",
|
||||
"UDC",
|
||||
"Cella",
|
||||
"Ubicazione",
|
||||
"Data",
|
||||
"InsUtente",
|
||||
"InsDataOra",
|
||||
"ModUtente",
|
||||
"ModDataOra",
|
||||
)
|
||||
self.tree = ttk.Treeview(wrap, columns=cols, show="headings")
|
||||
for col in cols:
|
||||
self.tree.heading(col, text=col)
|
||||
self.tree.column(col, width=90, anchor="w")
|
||||
self.tree.column("ID", width=70, anchor="e")
|
||||
self.tree.column("Tipo", width=55, anchor="center")
|
||||
self.tree.column("Rif", width=70, anchor="e")
|
||||
self.tree.column("UDC", width=110)
|
||||
self.tree.column("Cella", width=70, anchor="e")
|
||||
self.tree.column("Ubicazione", width=130)
|
||||
self.tree.column("Data", width=150)
|
||||
self.tree.column("InsDataOra", width=150)
|
||||
self.tree.column("ModDataOra", width=150)
|
||||
self.tree.tag_configure("versamento", background="#EAF7EA")
|
||||
self.tree.tag_configure("prelievo", background="#FFF0E6")
|
||||
self.tree.tag_configure("spedita", background="#FFECEC", foreground="#B00020")
|
||||
|
||||
sy = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
|
||||
sx = ttk.Scrollbar(wrap, orient="horizontal", command=self.tree.xview)
|
||||
self.tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
sy.grid(row=0, column=1, sticky="ns")
|
||||
sx.grid(row=1, column=0, sticky="ew")
|
||||
|
||||
def _do_search(self) -> None:
|
||||
udc = str(self.var_udc.get() or "").strip()
|
||||
params = {"udc": udc or None}
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_STORICO_UDC, params)
|
||||
|
||||
def _ok(res):
|
||||
rows = _rows_to_dicts(res)
|
||||
self._fill(rows)
|
||||
if not rows:
|
||||
messagebox.showinfo(
|
||||
loc_text("history.udc.msg.title", catalog=self._locale_catalog, default="Storico UDC"),
|
||||
loc_text("history.udc.msg.empty", catalog=self._locale_catalog, default="Nessun movimento trovato."),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
def _err(ex):
|
||||
messagebox.showerror(
|
||||
loc_text("history.udc.msg.title", catalog=self._locale_catalog, default="Storico UDC"),
|
||||
loc_text(
|
||||
"history.udc.msg.error",
|
||||
catalog=self._locale_catalog,
|
||||
default="Errore ricerca:\n{error}",
|
||||
).format(error=ex),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self._async.run(
|
||||
_job(),
|
||||
_ok,
|
||||
_err,
|
||||
busy=self._busy,
|
||||
message=loc_text("history.udc.busy", catalog=self._locale_catalog, default="Carico storico UDC..."),
|
||||
)
|
||||
|
||||
def _fill(self, rows: list[dict[str, Any]]) -> None:
|
||||
self.tree.delete(*self.tree.get_children(""))
|
||||
def _value(row: dict[str, Any], name: str) -> Any:
|
||||
value = row.get(name, "")
|
||||
return "" if value is None else value
|
||||
|
||||
for row in rows:
|
||||
tipo = str(row.get("Tipo") or "")
|
||||
tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else ""
|
||||
self.tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
_value(row, "ID"),
|
||||
tipo,
|
||||
_value(row, "IDRiferimento"),
|
||||
_value(row, "UDC"),
|
||||
_value(row, "IDCella"),
|
||||
_value(row, "Ubicazione"),
|
||||
_value(row, "DataMagazzino"),
|
||||
_value(row, "InsUtente"),
|
||||
_value(row, "InsDataOra"),
|
||||
_value(row, "ModUtente"),
|
||||
_value(row, "ModDataOra"),
|
||||
),
|
||||
tags=(tag,) if tag else (),
|
||||
)
|
||||
|
||||
|
||||
def open_storico_udc_window(parent: tk.Misc, db_client, session=None, initial_udc: str | None = None) -> tk.Misc:
|
||||
"""Open the UDC history window."""
|
||||
|
||||
win = StoricoUDCWindow(parent, db_client, session=session, initial_udc=initial_udc)
|
||||
place_window_fullsize_below_parent_later(parent, win)
|
||||
win.bind("<Escape>", lambda _e: win.destroy())
|
||||
return win
|
||||
@@ -5,7 +5,9 @@
|
||||
"launcher.open_layout": "Apre la vista layout delle corsie con celle, UDC presenti e menu operativo contestuale.",
|
||||
"launcher.open_multi_udc": "Apre la vista UDC fantasma per analizzare celle con piu' pallet e bonificare i candidati fantasma.",
|
||||
"launcher.open_search": "Apre la ricerca UDC per trovare rapidamente una unita' di carico e verificarne la posizione.",
|
||||
"launcher.open_history_udc": "Apre lo storico movimenti UDC per ricostruire carichi, scarichi, celle e utenti coinvolti.",
|
||||
"launcher.open_pickinglist": "Apre la gestione delle picking list per prenotare, controllare e aggiornare le liste di prelievo.",
|
||||
"launcher.open_history_pickinglist": "Apre lo storico picking list per consultare liste, stato operativo e dettaglio UDC.",
|
||||
"launcher.arrange_windows": "Dispone in cascata le finestre aperte seguendo l'ordine dei pulsanti del launcher.",
|
||||
"launcher.exit": "Chiude l'applicazione in modo pulito terminando la sessione utente e rilasciando la connessione condivisa al database.",
|
||||
"dbconfig.heading": "Spiega che qui si inseriscono i dati minimi per permettere al programma di collegarsi al database del magazzino al primo avvio.",
|
||||
@@ -37,7 +39,9 @@
|
||||
"launcher.open_layout": "Open the aisle layout view with cells, present UDCs and the operational context menu.",
|
||||
"launcher.open_multi_udc": "Open the ghost UDC view to inspect cells with multiple pallets and clean ghost candidates.",
|
||||
"launcher.open_search": "Open the UDC search window to quickly locate a load unit and verify its position.",
|
||||
"launcher.open_history_udc": "Open UDC movement history to review loads, unloads, cells and users involved.",
|
||||
"launcher.open_pickinglist": "Open picking list management to reserve, inspect and update picking lists.",
|
||||
"launcher.open_history_pickinglist": "Open picking-list history to inspect lists, operational status and UDC detail.",
|
||||
"launcher.arrange_windows": "Arrange open windows in cascade order following the launcher buttons.",
|
||||
"launcher.exit": "Close the application cleanly by ending the user session and releasing the shared database connection.",
|
||||
"dbconfig.heading": "Explain that this form collects the minimum data needed to connect the program to the warehouse database on first startup.",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"button_pady": 6,
|
||||
"info_padx": 6,
|
||||
"info_pady": [4, 2],
|
||||
"exit_icon_color": "#ca3d3d",
|
||||
"info_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 12,
|
||||
@@ -208,6 +209,38 @@
|
||||
"overlay_label_padding": [18, 14, 18, 8],
|
||||
"overlay_progress_padding": [18, 0, 18, 14]
|
||||
},
|
||||
"history_udc_window": {
|
||||
"window_geometry": "1100x720",
|
||||
"window_minsize": [900, 560],
|
||||
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||
"toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||
"toolbar_button_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"toolbar_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"entry_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"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]
|
||||
},
|
||||
"scarico_dialog": {
|
||||
"header_font": {
|
||||
"family": "Segoe UI",
|
||||
@@ -267,5 +300,37 @@
|
||||
"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],
|
||||
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||
"toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||
"toolbar_button_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"toolbar_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"entry_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ ALL_ACTIONS: FrozenSet[str] = frozenset(
|
||||
"launcher.open_layout",
|
||||
"launcher.open_multi_udc",
|
||||
"launcher.open_search",
|
||||
"launcher.open_history_udc",
|
||||
"launcher.open_pickinglist",
|
||||
"launcher.open_history_pickinglist",
|
||||
"launcher.arrange_windows",
|
||||
"launcher.exit",
|
||||
"reset_corsie.view",
|
||||
"search.view",
|
||||
"history_udc.view",
|
||||
"history_pickinglist.view",
|
||||
"multi_udc.view",
|
||||
"layout.view",
|
||||
"layout.carico",
|
||||
|
||||
8
warehouse.pyw
Normal file
8
warehouse.pyw
Normal file
@@ -0,0 +1,8 @@
|
||||
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||
|
||||
ensure_stdio("warehouse_main")
|
||||
|
||||
from main import run_app
|
||||
|
||||
|
||||
raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))
|
||||
Reference in New Issue
Block a user