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 asyncio
|
||||||
import threading
|
import threading
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
class _LoopHolder:
|
class _LoopHolder:
|
||||||
@@ -15,6 +16,7 @@ class _LoopHolder:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
self.closing = False
|
||||||
|
|
||||||
|
|
||||||
_GLOBAL = _LoopHolder()
|
_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
|
The loop is created lazily the first time the function is called and kept
|
||||||
alive for the lifetime of the application.
|
alive for the lifetime of the application.
|
||||||
"""
|
"""
|
||||||
if _GLOBAL.loop:
|
if _GLOBAL.loop and not _GLOBAL.closing:
|
||||||
return _GLOBAL.loop
|
return _GLOBAL.loop
|
||||||
|
|
||||||
ready = threading.Event()
|
ready = threading.Event()
|
||||||
@@ -35,7 +37,23 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
|||||||
_GLOBAL.loop = asyncio.new_event_loop()
|
_GLOBAL.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(_GLOBAL.loop)
|
asyncio.set_event_loop(_GLOBAL.loop)
|
||||||
ready.set()
|
ready.set()
|
||||||
|
try:
|
||||||
_GLOBAL.loop.run_forever()
|
_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 = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
|
||||||
_GLOBAL.thread.start()
|
_GLOBAL.thread.start()
|
||||||
@@ -46,7 +64,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
|||||||
def stop_global_loop():
|
def stop_global_loop():
|
||||||
"""Stop the shared loop and join the background thread if present."""
|
"""Stop the shared loop and join the background thread if present."""
|
||||||
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
||||||
|
_GLOBAL.closing = True
|
||||||
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
||||||
_GLOBAL.thread.join(timeout=2)
|
_GLOBAL.thread.join(timeout=2)
|
||||||
_GLOBAL.loop = None
|
_GLOBAL.loop = None
|
||||||
_GLOBAL.thread = 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.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.pool import NullPool
|
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:
|
try:
|
||||||
import orjson as _json
|
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 tkinter import messagebox, ttk
|
||||||
from typing import Callable
|
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_loop_singleton import get_global_loop, stop_global_loop
|
||||||
from async_msssql_query import AsyncMSSQLClient
|
from async_msssql_query import AsyncMSSQLClient
|
||||||
from barcode_repository import BarcodeRepository
|
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
|
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:
|
class BarcodeClientApp:
|
||||||
"""Single-window Tk barcode client modeled after the C# legacy form."""
|
"""Single-window Tk barcode client modeled after the C# legacy form."""
|
||||||
|
|
||||||
@@ -478,4 +475,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(run_with_fatal_log("Barcode WMS", main))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ SELECT TOP (1)
|
|||||||
Ubicazione,
|
Ubicazione,
|
||||||
Ordinamento,
|
Ordinamento,
|
||||||
IDStato
|
IDStato
|
||||||
FROM dbo.XMag_ViewPackingList
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
WHERE Ordinamento > 0
|
WHERE Ordinamento > 0
|
||||||
AND IDStato = :id_stato
|
AND IDStato = :id_stato
|
||||||
ORDER BY Ordinamento;
|
ORDER BY Ordinamento;
|
||||||
@@ -38,7 +38,7 @@ SELECT TOP (1)
|
|||||||
Ubicazione,
|
Ubicazione,
|
||||||
Ordinamento,
|
Ordinamento,
|
||||||
IDStato
|
IDStato
|
||||||
FROM dbo.XMag_ViewPackingList
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
WHERE Pallet = :pallet
|
WHERE Pallet = :pallet
|
||||||
ORDER BY Ordinamento;
|
ORDER BY Ordinamento;
|
||||||
"""
|
"""
|
||||||
@@ -65,6 +65,8 @@ EXEC dbo.sp_xMagGestioneMagazziniPallet
|
|||||||
@NumeroCella = :numero_cella,
|
@NumeroCella = :numero_cella,
|
||||||
@RC = @RC OUTPUT;
|
@RC = @RC OUTPUT;
|
||||||
|
|
||||||
|
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
@RC AS RC,
|
@RC AS RC,
|
||||||
:barcode_cella AS BarcodeCella,
|
: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,
|
MAX(Cella) AS Cella,
|
||||||
MIN(Ordinamento) AS Ordinamento,
|
MIN(Ordinamento) AS Ordinamento,
|
||||||
MAX(IDStato) AS IDStato
|
MAX(IDStato) AS IDStato
|
||||||
FROM dbo.ViewPackingListRestante
|
FROM dbo.py_ViewPackingListRestante
|
||||||
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||||
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SQL_PL_DETAILS = """
|
SQL_PL_DETAILS = """
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM ViewPackingListRestante
|
FROM dbo.py_ViewPackingListRestante
|
||||||
WHERE Documento = :Documento
|
WHERE Documento = :Documento
|
||||||
ORDER BY Ordinamento;
|
ORDER BY Ordinamento;
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ FROM (
|
|||||||
WHERE c.ID = :target_idcella
|
WHERE c.ID = :target_idcella
|
||||||
) AS target;
|
) AS target;
|
||||||
|
|
||||||
EXEC dbo.sp_ControllaPrenotazionePackingListPalletNew;
|
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
CAST(1 AS int) AS Ok,
|
CAST(1 AS int) AS Ok,
|
||||||
|
|||||||
32
locale.json
32
locale.json
@@ -7,7 +7,9 @@
|
|||||||
"launcher.layout": "Gestione Layout",
|
"launcher.layout": "Gestione Layout",
|
||||||
"launcher.multi_udc": "UDC Fantasma",
|
"launcher.multi_udc": "UDC Fantasma",
|
||||||
"launcher.search": "Ricerca UDC",
|
"launcher.search": "Ricerca UDC",
|
||||||
|
"launcher.history_udc": "Storico movimenti UDC",
|
||||||
"launcher.pickinglist": "Gestione Picking List",
|
"launcher.pickinglist": "Gestione Picking List",
|
||||||
|
"launcher.history_pickinglist": "Storico Picking List",
|
||||||
"launcher.arrange": "Ridisponi finestre",
|
"launcher.arrange": "Ridisponi finestre",
|
||||||
"launcher.exit": "Esci",
|
"launcher.exit": "Esci",
|
||||||
"launcher.already_running_title": "Warehouse",
|
"launcher.already_running_title": "Warehouse",
|
||||||
@@ -59,6 +61,20 @@
|
|||||||
"search.msg.no_results": "Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
"search.msg.no_results": "Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
||||||
"search.msg.error_title": "Errore ricerca",
|
"search.msg.error_title": "Errore ricerca",
|
||||||
"search.busy": "Cerco...",
|
"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.title": "Gestione Corsie - svuotamento celle per corsia",
|
||||||
"reset.label.aisle": "Corsia:",
|
"reset.label.aisle": "Corsia:",
|
||||||
"reset.button.refresh": "Carica",
|
"reset.button.refresh": "Carica",
|
||||||
@@ -104,7 +120,9 @@
|
|||||||
"launcher.layout": "Layout Management",
|
"launcher.layout": "Layout Management",
|
||||||
"launcher.multi_udc": "Ghost UDC",
|
"launcher.multi_udc": "Ghost UDC",
|
||||||
"launcher.search": "UDC Search",
|
"launcher.search": "UDC Search",
|
||||||
|
"launcher.history_udc": "UDC Movement History",
|
||||||
"launcher.pickinglist": "Picking List Management",
|
"launcher.pickinglist": "Picking List Management",
|
||||||
|
"launcher.history_pickinglist": "Picking List History",
|
||||||
"launcher.arrange": "Arrange windows",
|
"launcher.arrange": "Arrange windows",
|
||||||
"launcher.exit": "Exit",
|
"launcher.exit": "Exit",
|
||||||
"launcher.already_running_title": "Warehouse",
|
"launcher.already_running_title": "Warehouse",
|
||||||
@@ -156,6 +174,20 @@
|
|||||||
"search.msg.no_results": "No matches were found for the entered search keys.",
|
"search.msg.no_results": "No matches were found for the entered search keys.",
|
||||||
"search.msg.error_title": "Search error",
|
"search.msg.error_title": "Search error",
|
||||||
"search.busy": "Searching...",
|
"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.title": "Aisle Management - empty cells by aisle",
|
||||||
"reset.label.aisle": "Aisle:",
|
"reset.label.aisle": "Aisle:",
|
||||||
"reset.button.refresh": "Load",
|
"reset.button.refresh": "Load",
|
||||||
|
|||||||
121
main.py
121
main.py
@@ -11,6 +11,10 @@ import sys
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||||
|
|
||||||
|
ensure_stdio("warehouse_main")
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from tkinter import messagebox
|
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 locale_text import load_locale_catalog, text as loc_text
|
||||||
from reset_corsie import open_reset_corsie_window
|
from reset_corsie import open_reset_corsie_window
|
||||||
from search_pallets import open_search_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 tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
|
||||||
from ui_theme import theme_font, theme_section, theme_value
|
from ui_theme import theme_font, theme_section, theme_value
|
||||||
from user_session import UserSession, create_user_session
|
from user_session import UserSession, create_user_session
|
||||||
@@ -45,9 +51,9 @@ BYPASS_LOGIN_USER = {
|
|||||||
"codice_unita": "U1",
|
"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()
|
_loop = get_global_loop()
|
||||||
asyncio.set_event_loop(_loop)
|
|
||||||
|
|
||||||
|
|
||||||
def _noop(*args, **kwargs):
|
def _noop(*args, **kwargs):
|
||||||
@@ -148,7 +154,9 @@ class Launcher(ctk.CTk):
|
|||||||
"layout",
|
"layout",
|
||||||
"multi_udc",
|
"multi_udc",
|
||||||
"search",
|
"search",
|
||||||
|
"storico_udc",
|
||||||
"pickinglist",
|
"pickinglist",
|
||||||
|
"storico_pickinglist",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
|
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
|
||||||
@@ -164,6 +172,9 @@ class Launcher(ctk.CTk):
|
|||||||
self._is_cascading = False
|
self._is_cascading = False
|
||||||
self._focus_restore_pending: set[str] = set()
|
self._focus_restore_pending: set[str] = set()
|
||||||
self._restore_suppressed_until = 0.0
|
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(
|
self.title(
|
||||||
f"{loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse 1.0.0')} - {self.session.display_name}"
|
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",
|
"reset_corsie",
|
||||||
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
|
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
|
||||||
"launcher.open_reset_corsie",
|
"launcher.open_reset_corsie",
|
||||||
lambda: self._open_child_window(
|
lambda: self._open_or_focus_child_window(
|
||||||
"reset_corsie",
|
"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",
|
"layout",
|
||||||
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
|
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
|
||||||
"launcher.open_layout",
|
"launcher.open_layout",
|
||||||
lambda: self._open_child_window(
|
lambda: self._open_or_focus_child_window(
|
||||||
"layout",
|
"layout",
|
||||||
open_layout_window(self, self.db_client, session=self.session),
|
lambda: open_layout_window(self, self.db_client, session=self.session),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"multi_udc",
|
"multi_udc",
|
||||||
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
|
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
|
||||||
"launcher.open_multi_udc",
|
"launcher.open_multi_udc",
|
||||||
lambda: self._open_child_window(
|
lambda: self._open_or_focus_child_window(
|
||||||
"multi_udc",
|
"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",
|
"search",
|
||||||
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
|
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
|
||||||
"launcher.open_search",
|
"launcher.open_search",
|
||||||
lambda: self._open_child_window(
|
lambda: self._open_or_focus_child_window(
|
||||||
"search",
|
"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",
|
"pickinglist",
|
||||||
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
|
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
|
||||||
"launcher.open_pickinglist",
|
"launcher.open_pickinglist",
|
||||||
lambda: self._open_child_window(
|
lambda: self._open_or_focus_child_window(
|
||||||
"pickinglist",
|
"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):
|
for idx, (_key, label, permission, callback) in enumerate(actions):
|
||||||
row = 1 + (idx // max_buttons_per_row)
|
row = 1 + (idx // max_buttons_per_row)
|
||||||
column = 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(
|
button = ctk.CTkButton(
|
||||||
wrap,
|
wrap,
|
||||||
text=label,
|
text=text,
|
||||||
font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
|
font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
|
||||||
state="normal" if self.session.can(permission) else "disabled",
|
state="normal" if self.session.can(permission) else "disabled",
|
||||||
command=callback,
|
command=callback,
|
||||||
|
**button_options,
|
||||||
)
|
)
|
||||||
button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
|
button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
|
||||||
tip = tooltip_text(permission, catalog=self._tooltip_catalog)
|
tip = tooltip_text(permission, catalog=self._tooltip_catalog)
|
||||||
@@ -282,6 +322,18 @@ class Launcher(ctk.CTk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
def _apply_dynamic_geometry(self) -> None:
|
||||||
"""Size the launcher around its current content and keep it docked at the top."""
|
"""Size the launcher around its current content and keep it docked at the top."""
|
||||||
|
|
||||||
@@ -334,6 +386,34 @@ class Launcher(ctk.CTk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
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."""
|
"""Remove stale references to child windows that have been closed."""
|
||||||
|
|
||||||
@@ -460,7 +540,9 @@ class Launcher(ctk.CTk):
|
|||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def run_app() -> int:
|
||||||
|
"""Run the backoffice application entry point."""
|
||||||
|
|
||||||
ctk.set_appearance_mode("light")
|
ctk.set_appearance_mode("light")
|
||||||
ctk.set_default_color_theme("green")
|
ctk.set_default_color_theme("green")
|
||||||
if not _acquire_single_instance_mutex():
|
if not _acquire_single_instance_mutex():
|
||||||
@@ -479,7 +561,7 @@ if __name__ == "__main__":
|
|||||||
stop_global_loop()
|
stop_global_loop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise SystemExit(0)
|
return 0
|
||||||
|
|
||||||
db_cfg = ensure_db_config(_loop)
|
db_cfg = ensure_db_config(_loop)
|
||||||
if db_cfg is None:
|
if db_cfg is None:
|
||||||
@@ -487,7 +569,7 @@ if __name__ == "__main__":
|
|||||||
stop_global_loop()
|
stop_global_loop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise SystemExit(0)
|
return 0
|
||||||
|
|
||||||
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
|
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
|
||||||
|
|
||||||
@@ -511,7 +593,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if session is None:
|
if session is None:
|
||||||
_shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
|
_shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
|
||||||
raise SystemExit(0)
|
return 0
|
||||||
|
|
||||||
_destroy_tk_root(bootstrap)
|
_destroy_tk_root(bootstrap)
|
||||||
|
|
||||||
@@ -519,3 +601,8 @@ if __name__ == "__main__":
|
|||||||
Launcher(session, db_app).mainloop()
|
Launcher(session, db_app).mainloop()
|
||||||
finally:
|
finally:
|
||||||
_shutdown_runtime(bootstrap=None, dispose_db=True)
|
_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()
|
@_log_call()
|
||||||
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult:
|
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:
|
try:
|
||||||
azione = str(Azione or "P").strip().upper()
|
azione = str(Azione or "P").strip().upper()
|
||||||
if azione not in ("P", "S"):
|
if azione not in ("P", "S"):
|
||||||
@@ -268,7 +268,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
|||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
DECLARE @RC int = 0;
|
DECLARE @RC int = 0;
|
||||||
|
|
||||||
EXEC dbo.sp_xExePackingListPallet
|
EXEC dbo.py_sp_xExePackingListPallet
|
||||||
@IDOperatore = :IDOperatore,
|
@IDOperatore = :IDOperatore,
|
||||||
@Documento = :Documento,
|
@Documento = :Documento,
|
||||||
@Azione = :Azione,
|
@Azione = :Azione,
|
||||||
@@ -276,7 +276,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
|||||||
|
|
||||||
SELECT CAST(@RC AS int) AS RC;
|
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"):
|
if not hasattr(db, "query_json"):
|
||||||
raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.")
|
raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.")
|
||||||
res = await db.query_json(
|
res = await db.query_json(
|
||||||
@@ -288,7 +288,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
|
|||||||
rows = []
|
rows = []
|
||||||
if isinstance(res, dict):
|
if isinstance(res, dict):
|
||||||
rows = res.get("rows", []) or []
|
rows = res.get("rows", []) or []
|
||||||
_log_dataset("sp_xExePackingListPallet", rows)
|
_log_dataset("py_sp_xExePackingListPallet", rows)
|
||||||
rc = 0
|
rc = 0
|
||||||
if rows and isinstance(rows[0], dict):
|
if rows and isinstance(rows[0], dict):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
aioodbc
|
aioodbc==0.5.0
|
||||||
customtkinter
|
customtkinter==5.2.2
|
||||||
loguru
|
loguru
|
||||||
openpyxl
|
openpyxl==3.1.5
|
||||||
orjson
|
orjson
|
||||||
pyodbc
|
pyodbc==5.3.0
|
||||||
SQLAlchemy
|
SQLAlchemy==2.0.49
|
||||||
tksheet
|
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_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_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_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_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.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.",
|
"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.",
|
"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_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_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_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_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.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.",
|
"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.",
|
"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,
|
"button_pady": 6,
|
||||||
"info_padx": 6,
|
"info_padx": 6,
|
||||||
"info_pady": [4, 2],
|
"info_pady": [4, 2],
|
||||||
|
"exit_icon_color": "#ca3d3d",
|
||||||
"info_font": {
|
"info_font": {
|
||||||
"family": "Segoe UI",
|
"family": "Segoe UI",
|
||||||
"size": 12,
|
"size": 12,
|
||||||
@@ -208,6 +209,38 @@
|
|||||||
"overlay_label_padding": [18, 14, 18, 8],
|
"overlay_label_padding": [18, 14, 18, 8],
|
||||||
"overlay_progress_padding": [18, 0, 18, 14]
|
"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": {
|
"scarico_dialog": {
|
||||||
"header_font": {
|
"header_font": {
|
||||||
"family": "Segoe UI",
|
"family": "Segoe UI",
|
||||||
@@ -267,5 +300,37 @@
|
|||||||
"overlay_progress_width": 220,
|
"overlay_progress_width": 220,
|
||||||
"overlay_label_padding": [18, 14, 18, 8],
|
"overlay_label_padding": [18, 14, 18, 8],
|
||||||
"overlay_progress_padding": [18, 0, 18, 14]
|
"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_layout",
|
||||||
"launcher.open_multi_udc",
|
"launcher.open_multi_udc",
|
||||||
"launcher.open_search",
|
"launcher.open_search",
|
||||||
|
"launcher.open_history_udc",
|
||||||
"launcher.open_pickinglist",
|
"launcher.open_pickinglist",
|
||||||
|
"launcher.open_history_pickinglist",
|
||||||
"launcher.arrange_windows",
|
"launcher.arrange_windows",
|
||||||
"launcher.exit",
|
"launcher.exit",
|
||||||
"reset_corsie.view",
|
"reset_corsie.view",
|
||||||
"search.view",
|
"search.view",
|
||||||
|
"history_udc.view",
|
||||||
|
"history_pickinglist.view",
|
||||||
"multi_udc.view",
|
"multi_udc.view",
|
||||||
"layout.view",
|
"layout.view",
|
||||||
"layout.carico",
|
"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