Compare commits

..

14 Commits

20 changed files with 2583 additions and 167 deletions

View File

@@ -0,0 +1,43 @@
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U') IS NULL
BEGIN
CREATE TABLE dbo.py_BarcodePickingListSkip (
ID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_py_BarcodePickingListSkip PRIMARY KEY,
Documento varchar(50) COLLATE Latin1_General_CI_AS NOT NULL,
Pallet varchar(50) COLLATE Latin1_General_CI_AS NOT NULL,
IDStato int NOT NULL,
IDOperatore int NOT NULL,
DataOra datetime2(0) NOT NULL CONSTRAINT DF_py_BarcodePickingListSkip_DataOra DEFAULT SYSDATETIME(),
Motivo nvarchar(200) NULL,
Risolto bit NOT NULL CONSTRAINT DF_py_BarcodePickingListSkip_Risolto DEFAULT 0,
RisoltoDa int NULL,
RisoltoDataOra datetime2(0) NULL
);
END;
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = N'UX_py_BarcodePickingListSkip_Open'
AND object_id = OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U')
)
BEGIN
CREATE UNIQUE INDEX UX_py_BarcodePickingListSkip_Open
ON dbo.py_BarcodePickingListSkip (Documento, Pallet, IDStato)
WHERE Risolto = 0;
END;
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = N'IX_py_BarcodePickingListSkip_Queue'
AND object_id = OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U')
)
BEGIN
CREATE INDEX IX_py_BarcodePickingListSkip_Queue
ON dbo.py_BarcodePickingListSkip (IDStato, Documento, Pallet, Risolto);
END;
COMMIT TRANSACTION;

View File

@@ -0,0 +1,531 @@
/*
Patch cumulativa DB online - ramo Python WMS
Da eseguire una sola volta in SSMS sul database Mediseawall.
Questa patch NON modifica gli oggetti legacy usati dal programma C#.
Include:
- dbo.PyPickingListReservation
- dbo.py_XMag_ViewPackingList
- dbo.py_ViewPackingListRestante
- dbo.py_sp_xExePackingListPallet
- dbo.py_sp_xExePackingListPalletPrenota
- dbo.py_sp_ControllaPrenotazionePackingListPalletNew
- dbo.py_vPreparaPackingListSAMA1
- dbo.py_vPreparaPackingList
- dbo.py_XMag_ViewPackingListStorico
- dbo.py_BarcodePickingListSkip
Nota storico:
dbo.py_vPreparaPackingListSAMA1 usa ANNDOC >= YEAR(GETDATE())
per mantenere visibili solo le picking list dall'inizio dell'anno corrente.
*/
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
/* ============================================================
1. Stato prenotazione picking list ramo Python
============================================================ */
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
/* ============================================================
2. Viste operative picking list ramo Python
============================================================ */
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
/* ============================================================
3. Stored procedure operative ramo Python
============================================================ */
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
/* ============================================================
4. Viste storico picking list ramo Python
============================================================ */
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
/* ============================================================
5. Tabella salta UDC barcode picking list
============================================================ */
IF OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U') IS NULL
BEGIN
CREATE TABLE dbo.py_BarcodePickingListSkip (
ID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_py_BarcodePickingListSkip PRIMARY KEY,
Documento varchar(50) COLLATE Latin1_General_CI_AS NOT NULL,
Pallet varchar(50) COLLATE Latin1_General_CI_AS NOT NULL,
IDStato int NOT NULL,
IDOperatore int NOT NULL,
DataOra datetime2(0) NOT NULL CONSTRAINT DF_py_BarcodePickingListSkip_DataOra DEFAULT SYSDATETIME(),
Motivo nvarchar(200) NULL,
Risolto bit NOT NULL CONSTRAINT DF_py_BarcodePickingListSkip_Risolto DEFAULT 0,
RisoltoDa int NULL,
RisoltoDataOra datetime2(0) NULL
);
END;
GO
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = N'UX_py_BarcodePickingListSkip_Open'
AND object_id = OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U')
)
BEGIN
CREATE UNIQUE INDEX UX_py_BarcodePickingListSkip_Open
ON dbo.py_BarcodePickingListSkip (Documento, Pallet, IDStato)
WHERE Risolto = 0;
END;
GO
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = N'IX_py_BarcodePickingListSkip_Queue'
AND object_id = OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U')
)
BEGIN
CREATE INDEX IX_py_BarcodePickingListSkip_Queue
ON dbo.py_BarcodePickingListSkip (IDStato, Documento, Pallet, Risolto);
END;
GO
PRINT 'Patch cumulativa Python WMS completata.';
GO

View File

@@ -10,9 +10,13 @@ cross-loop errors.
from __future__ import annotations
import asyncio
import inspect
import json
import logging
import os
import time
import urllib.parse
from pathlib import Path
from typing import Any, Dict, Optional
from sqlalchemy import text
@@ -22,6 +26,59 @@ from version_info import module_version
__version__ = module_version(__name__)
QUERY_PROFILER_LOG = Path(__file__).with_name("warehouse_query_profiler.log")
QUERY_PROFILER_CONFIG_PATH = Path(__file__).with_name("db_connection.json")
def _load_query_profiler_config() -> dict[str, Any]:
"""Read profiler settings from db_connection.json, with env overrides."""
config: dict[str, Any] = {
"enabled": False,
"slow_ms": 0,
"sql_limit": 4000,
}
try:
data = json.loads(QUERY_PROFILER_CONFIG_PATH.read_text(encoding="utf-8"))
section = data.get("query_profiler") if isinstance(data, dict) else None
if isinstance(section, dict):
config.update(section)
except Exception:
pass
if "WAREHOUSE_QUERY_PROFILER" in os.environ:
config["enabled"] = os.environ.get("WAREHOUSE_QUERY_PROFILER", "1").strip().lower() not in {
"0",
"false",
"no",
"off",
}
if "WAREHOUSE_QUERY_PROFILER_SLOW_MS" in os.environ:
config["slow_ms"] = os.environ.get("WAREHOUSE_QUERY_PROFILER_SLOW_MS", "0")
if "WAREHOUSE_QUERY_PROFILER_SQL_LIMIT" in os.environ:
config["sql_limit"] = os.environ.get("WAREHOUSE_QUERY_PROFILER_SQL_LIMIT", "4000")
return config
def _query_profiler_enabled() -> bool:
value = _load_query_profiler_config().get("enabled", False)
if isinstance(value, str):
return value.strip().lower() not in {"0", "false", "no", "off"}
return bool(value)
def _query_profiler_slow_ms() -> float:
try:
return float(_load_query_profiler_config().get("slow_ms", 0) or 0)
except Exception:
return 0.0
def _query_profiler_sql_limit() -> int:
try:
return int(_load_query_profiler_config().get("sql_limit", 4000) or 4000)
except Exception:
return 4000
try:
import pyodbc
@@ -76,6 +133,83 @@ def make_mssql_dsn(
return f"mssql+aioodbc:///?odbc_connect={urllib.parse.quote_plus(odbc)}"
def _compact_sql(sql: str, *, limit: int | None = None) -> str:
"""Collapse SQL whitespace so profiler entries stay readable."""
if limit is None:
limit = _query_profiler_sql_limit()
text_value = " ".join(str(sql or "").split())
if limit > 0 and len(text_value) > limit:
return text_value[:limit] + "...<truncated>"
return text_value
def _profile_params(params: Optional[Dict[str, Any]]) -> str:
"""Serialize SQL parameters for diagnostics."""
if not params:
return "{}"
try:
return _dumps(params)
except Exception:
return repr(params)
def _query_caller() -> str:
"""Return the first external Python frame that triggered the DB call."""
current_file = Path(__file__).resolve()
for frame in inspect.stack(context=0)[2:]:
try:
frame_file = Path(frame.filename).resolve()
except Exception:
continue
if frame_file == current_file:
continue
return f"{frame_file.name}:{frame.lineno}:{frame.function}"
return "unknown"
def _append_query_profile(
*,
method: str,
elapsed_ms: float,
rows: int | None,
rowcount: int | None,
commit: bool,
ok: bool,
sql: str,
params: Optional[Dict[str, Any]],
caller: str,
error: str = "",
) -> None:
"""Append one query timing line to the local profiler log."""
if not _query_profiler_enabled():
return
slow_ms = _query_profiler_slow_ms()
if ok and slow_ms > 0 and elapsed_ms < slow_ms:
return
try:
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
row_info = f"rows={rows}" if rows is not None else f"rowcount={rowcount}"
lines = [
(
f"{timestamp} | {elapsed_ms:.3f} ms | {method} | "
f"{row_info} | commit={int(commit)} | ok={int(ok)} | caller={caller}"
),
f"PARAMS: {_profile_params(params)}",
f"SQL: {_compact_sql(sql)}",
]
if error:
lines.append(f"ERROR: {error}")
with QUERY_PROFILER_LOG.open("a", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n---\n")
except Exception:
# Profiling must never interfere with warehouse operations.
pass
class AsyncMSSQLClient:
"""Thin async query client for SQL Server.
@@ -152,23 +286,83 @@ class AsyncMSSQLClient:
"""
await self._ensure_engine()
t0 = time.perf_counter()
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
res = await conn.execute(text(sql), params or {})
rows = res.fetchall()
cols = list(res.keys())
caller = _query_caller()
try:
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
res = await conn.execute(text(sql), params or {})
rows = res.fetchall()
cols = list(res.keys())
except Exception as exc:
elapsed_ms = round((time.perf_counter() - t0) * 1000, 3)
_append_query_profile(
method="query_json",
elapsed_ms=elapsed_ms,
rows=None,
rowcount=None,
commit=commit,
ok=False,
sql=sql,
params=params,
caller=caller,
error=repr(exc),
)
raise
if as_dict_rows:
rows_out = [dict(zip(cols, row)) for row in rows]
else:
rows_out = [list(row) for row in rows]
elapsed_ms = round((time.perf_counter() - t0) * 1000, 3)
_append_query_profile(
method="query_json",
elapsed_ms=elapsed_ms,
rows=len(rows_out),
rowcount=None,
commit=commit,
ok=True,
sql=sql,
params=params,
caller=caller,
)
return {
"columns": cols,
"rows": rows_out,
"elapsed_ms": round((time.perf_counter() - t0) * 1000, 3),
"elapsed_ms": elapsed_ms,
}
async def exec(self, sql: str, params: Optional[Dict[str, Any]] = None, *, commit: bool = False) -> int:
"""Execute a DML statement and return its row count."""
await self._ensure_engine()
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
res = await conn.execute(text(sql), params or {})
return res.rowcount or 0
t0 = time.perf_counter()
caller = _query_caller()
try:
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
res = await conn.execute(text(sql), params or {})
rowcount = res.rowcount or 0
except Exception as exc:
elapsed_ms = round((time.perf_counter() - t0) * 1000, 3)
_append_query_profile(
method="exec",
elapsed_ms=elapsed_ms,
rows=None,
rowcount=None,
commit=commit,
ok=False,
sql=sql,
params=params,
caller=caller,
error=repr(exc),
)
raise
elapsed_ms = round((time.perf_counter() - t0) * 1000, 3)
_append_query_profile(
method="exec",
elapsed_ms=elapsed_ms,
rows=None,
rowcount=rowcount,
commit=commit,
ok=True,
sql=sql,
params=params,
caller=caller,
)
return rowcount

View File

@@ -68,6 +68,10 @@ class BarcodeClientApp:
self._auto_advance_id: str | None = None
self._pallet_auto_focus_id: str | None = None
self._paused_queue_id: int | None = None
self._paused_priority_state: BarcodeViewState | None = None
self._pallet_user_input_serial = 0
self._priority_loaded_input_serial = 0
self._priority_expected_pallet = ""
self._status_colors = {
"red": "#f4cccc",
"green": "#d9ead3",
@@ -237,16 +241,20 @@ class BarcodeClientApp:
parent.columnconfigure(1, weight=1)
if scanned:
self.pallet_entry = entry
self.pallet_entry.bind("<KeyPress>", self._on_pallet_keypress, add="+")
self.scanned_var.trace_add("write", lambda *_: self._on_scanned_var_changed())
else:
self.destination_entry = entry
self.destination_var.trace_add("write", lambda *_: self._limit_var(self.destination_var, 8))
self.destination_var.trace_add("write", lambda *_: self._on_destination_var_changed())
def _limit_var(self, variable: tk.StringVar, max_len: int) -> None:
value = str(variable.get() or "")
if len(value) > max_len:
variable.set(value[:max_len])
def _on_pallet_keypress(self, _event=None) -> None:
self._pallet_user_input_serial += 1
def _on_scanned_var_changed(self) -> None:
self._limit_var(self.scanned_var, 8)
if self._pallet_auto_focus_id is not None:
@@ -260,6 +268,10 @@ class BarcodeClientApp:
return
self._pallet_auto_focus_id = self.root.after(80, self._auto_focus_destination_after_scan)
def _on_destination_var_changed(self) -> None:
self._limit_var(self.destination_var, 8)
self._refresh_action_labels()
def _auto_focus_destination_after_scan(self) -> None:
self._pallet_auto_focus_id = None
pallet = str(self.scanned_var.get() or "").strip()
@@ -269,6 +281,11 @@ class BarcodeClientApp:
return
if getattr(self.service.state, "mode", "") == "confirm":
return
if getattr(self.service.state, "mode", "") in ("priority_high", "priority_low"):
expected = str(getattr(self.service.state, "expected_pallet", "") or "").strip()
if expected and pallet == expected and self._is_fresh_priority_scan(expected):
self._submit()
return
if bool(getattr(self.service.state, "destination_readonly", False)):
return
self._focus_destination_input()
@@ -371,7 +388,9 @@ class BarcodeClientApp:
except Exception:
pass
self._pallet_auto_focus_id = None
self.queue_var.set(state.queue_label)
pause_context = self._paused_queue_id is not None and state.mode not in ("priority_high", "priority_low")
effective_queue_label = "Pausa PL" if pause_context else state.queue_label
self.queue_var.set(effective_queue_label)
self.destination_var.set(state.destination_barcode)
self.scanned_var.set(state.scanned_pallet)
self.info1_var.set(self._status_text_with_wait_hint(state))
@@ -380,21 +399,28 @@ class BarcodeClientApp:
self.info4_var.set(state.expected_pallet)
self.status_band.configure(bg=state.status_color or self._status_colors["red"])
if state.mode in ("priority_high", "priority_low"):
self._priority_loaded_input_serial = self._pallet_user_input_serial
self._priority_expected_pallet = str(state.expected_pallet or "").strip()
else:
self._priority_expected_pallet = ""
try:
if str(state.queue_label or "") != "Pausa PL":
if str(effective_queue_label or "") != "Pausa PL":
self._paused_queue_id = None
resumable_queue = self._resumable_queue_from_state(state)
if state.mode in ("priority_high", "priority_low") or resumable_queue is not None:
self.btn_submit.configure(text="[F3] Pausa PL")
else:
self.btn_submit.configure(text="[F3] Carica")
queue_buttons_state = "disabled" if str(state.queue_label or "") == "Pausa PL" else "normal"
self._refresh_action_labels(state)
queue_buttons_state = "disabled" if str(effective_queue_label or "") == "Pausa PL" else "normal"
self.btn_f1.configure(state=queue_buttons_state)
self.btn_f2.configure(state=queue_buttons_state)
except Exception:
pass
destination_readonly = bool(getattr(state, "destination_readonly", False))
destination_readonly = False if pause_context else bool(getattr(state, "destination_readonly", False))
try:
self.destination_entry.configure(state="normal")
if destination_readonly:
@@ -410,7 +436,26 @@ class BarcodeClientApp:
lambda q=next_queue: self._start_queue(q),
)
self.root.after(20, self._focus_primary_input)
if bool(getattr(state, "focus_destination", False)):
self.root.after(20, self._focus_destination_input)
else:
self.root.after(20, self._focus_primary_input)
def _refresh_action_labels(self, state: BarcodeViewState | None = None) -> None:
current_state = state or self.service.state
try:
if current_state.mode in ("priority_high", "priority_low"):
self.btn_unload.configure(text="[F4] Salta UDC")
return
destination = str(self.destination_var.get() or "").strip()
if destination == self.NON_SCAFFALATA_BARCODE:
self.btn_unload.configure(text="[F4] Non scaff.")
elif destination:
self.btn_unload.configure(text="[F4] Dissocia")
else:
self.btn_unload.configure(text="[F4] Scarica")
except Exception:
pass
def _focus_primary_input(self) -> None:
try:
@@ -441,6 +486,14 @@ class BarcodeClientApp:
except Exception:
pass
def _is_fresh_priority_scan(self, expected: str) -> bool:
mode = str(getattr(self.service.state, "mode", "") or "")
if mode not in ("priority_high", "priority_low"):
return False
if str(expected or "").strip() != self._priority_expected_pallet:
return False
return self._pallet_user_input_serial > self._priority_loaded_input_serial
def _on_pallet_enter(self, _event=None) -> str:
pallet = str(self.scanned_var.get() or "").strip()
destination = str(self.destination_var.get() or "").strip()
@@ -448,7 +501,15 @@ class BarcodeClientApp:
return "break"
if destination in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE):
if bool(getattr(self.service.state, "destination_readonly", False)):
self._submit()
mode = str(getattr(self.service.state, "mode", "") or "")
if mode in ("priority_high", "priority_low"):
expected = str(getattr(self.service.state, "expected_pallet", "") or "").strip()
if expected and pallet == expected and self._is_fresh_priority_scan(expected):
self._submit()
else:
self._focus_primary_input()
else:
self._submit()
else:
self._focus_destination_input()
return "break"
@@ -458,11 +519,12 @@ class BarcodeClientApp:
def _on_destination_enter(self, _event=None) -> str:
pallet = str(self.scanned_var.get() or "").strip()
destination = str(self.destination_var.get() or "").strip()
if pallet and destination:
self._submit()
else:
if not pallet:
self._focus_primary_input()
else:
self.info1_var.set("F3 per associare, F4 per disassociare")
self.status_band.configure(bg=self._status_colors["yellow"])
self._focus_destination_input()
return "break"
def _on_unload_key(self, _event=None) -> str:
@@ -471,6 +533,12 @@ class BarcodeClientApp:
def _on_escape_key(self, _event=None) -> str:
if self._is_priority_pause() and self._paused_queue_id is not None:
if self._paused_priority_state is not None:
state = self.service.resume_priority_state(self._paused_priority_state)
self._paused_priority_state = None
self._paused_queue_id = None
self._apply_state(state)
return "break"
self._start_queue(self._paused_queue_id, allow_during_pause=True)
return "break"
@@ -482,15 +550,18 @@ class BarcodeClientApp:
mode = getattr(self.service.state, "mode", "")
if mode == "priority_high":
self._paused_queue_id = 1
self._paused_priority_state = self.service.state
self._apply_state(self.service.begin_priority_pause(1))
return
if mode == "priority_low":
self._paused_queue_id = 0
self._paused_priority_state = self.service.state
self._apply_state(self.service.begin_priority_pause(0))
return
resumable_queue = self._resumable_queue_from_state(self.service.state)
if resumable_queue is not None:
self._paused_queue_id = resumable_queue
self._paused_priority_state = None
self._apply_state(self.service.begin_priority_pause(resumable_queue))
return
self._submit()
@@ -499,12 +570,14 @@ class BarcodeClientApp:
pallet = str(self.scanned_var.get() or "").strip()
destination = str(self.destination_var.get() or "").strip()
mode = getattr(self.service.state, "mode", "")
if pallet and mode in ("priority_high", "priority_low") and destination == self.SHIPPED_BARCODE:
# Legacy barcode flow: F4/Scarica confirms the prepared unload destination.
self._submit()
if mode in ("priority_high", "priority_low"):
self._skip_current_picking_pallet()
return
if pallet:
self._submit_unload_with_source_check()
if destination == self.NON_SCAFFALATA_BARCODE:
self._submit()
else:
self._submit_unload_with_source_check()
return
if self._is_priority_pause():
return
@@ -536,6 +609,12 @@ class BarcodeClientApp:
busy_message="In esecuzione...",
)
def _skip_current_picking_pallet(self) -> None:
self._run_async(
lambda: self.service.skip_current_picking_pallet(),
busy_message="Registro salto UDC...",
)
def _run_async(self, coro_factory: Callable[[], object], busy_message: str) -> None:
if self._pending is not None and not self._pending.done():
return
@@ -578,17 +657,15 @@ class BarcodeClientApp:
result = future.result()
except Exception as exc:
log_exception("Barcode WMS", exc, context="barcode async operation")
current = self.service.state
current.status_text = "Transazione non completata, ripeti l'operazione."
current.status_color = "#f4cccc"
self._apply_state(current)
messagebox.showerror(
"Barcode WMS",
"Operazione non completata.\n\n"
"Ripeti la lettura o avvisa il responsabile.\n"
"Il dettaglio tecnico e' stato scritto nel log.",
parent=self.root,
self._set_busy(True, "Verifico esito...")
self._pending = asyncio.run_coroutine_threadsafe(
self.service.reconcile_after_submit_exception(
scanned_pallet=self.scanned_var.get(),
destination_barcode=self.destination_var.get(),
),
self.loop,
)
self.root.after(40, lambda err=exc: self._poll_recovery_future(err))
return
if isinstance(result, BarcodeActionResult):
@@ -598,6 +675,46 @@ class BarcodeClientApp:
elif isinstance(result, BarcodeViewState):
self._apply_state(result)
def _show_transaction_error(self) -> None:
messagebox.showerror(
"Barcode WMS",
"Operazione non completata.\n\n"
"Ripeti la lettura o avvisa il responsabile.\n"
"Il dettaglio tecnico e' stato scritto nel log.",
parent=self.root,
)
def _poll_recovery_future(self, original_exception: Exception) -> None:
if self._pending is None:
self._set_busy(False)
self._show_transaction_error()
return
if not self._pending.done():
self.root.after(40, lambda err=original_exception: self._poll_recovery_future(err))
return
future = self._pending
self._pending = None
self._set_busy(False)
try:
result = future.result()
except Exception as exc:
log_exception("Barcode WMS", exc, context=f"barcode recovery after {original_exception!r}")
current = self.service.state
current.status_text = "Transazione non completata, ripeti l'operazione."
current.status_color = "#f4cccc"
self._apply_state(current)
self._show_transaction_error()
return
if isinstance(result, BarcodeActionResult):
self._apply_state(result.state)
if not result.ok:
self.root.bell()
self._show_transaction_error()
elif isinstance(result, BarcodeViewState):
self._apply_state(result)
def _shutdown(self) -> None:
try:
if self._pending is not None and not self._pending.done():

View File

@@ -26,9 +26,19 @@ SELECT TOP (1)
Ubicazione,
Ordinamento,
IDStato
FROM dbo.py_XMag_ViewPackingList
FROM dbo.py_XMag_ViewPackingList AS pl
WHERE Ordinamento > 0
AND IDStato = :id_stato
AND NOT EXISTS (
SELECT 1
FROM dbo.py_BarcodePickingListSkip AS s
WHERE s.Documento COLLATE Latin1_General_CI_AS =
CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS
AND s.Pallet COLLATE Latin1_General_CI_AS =
CAST(pl.Pallet AS varchar(50)) COLLATE Latin1_General_CI_AS
AND s.IDStato = pl.IDStato
AND s.Risolto = 0
)
ORDER BY Ordinamento;
"""
@@ -44,7 +54,7 @@ SELECT TOP (1)
Ordinamento,
IDStato
FROM dbo.py_XMag_ViewPackingList
WHERE Pallet = :pallet
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY Ordinamento;
"""
@@ -55,7 +65,7 @@ SELECT TOP (1)
Prodotto,
Descrizione
FROM dbo.vXTracciaProdotti
WHERE Pallet = :pallet
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY Lotto;
"""
@@ -69,7 +79,7 @@ SELECT TOP (1)
FROM dbo.XMag_GiacenzaPallet AS g
LEFT JOIN dbo.Celle AS c
ON c.ID = g.IDCella
WHERE g.BarcodePallet = :pallet;
WHERE g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS;
"""
SQL_OPEN_LOCATIONS_BY_PALLET = """
@@ -82,10 +92,47 @@ SELECT
FROM dbo.XMag_GiacenzaPallet AS g
LEFT JOIN dbo.Celle AS c
ON c.ID = g.IDCella
WHERE g.BarcodePallet = :pallet
WHERE g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY g.IDCella;
"""
SQL_OPEN_PALLETS_BY_CELL = """
SELECT
g.BarcodePallet,
g.IDCella
FROM dbo.XMag_GiacenzaPallet AS g
WHERE g.IDCella = :id_cella
ORDER BY g.BarcodePallet;
"""
SQL_CLOSED_PICKING_BY_PALLET = """
SELECT TOP (1)
g.BarcodePallet,
g.NumeroPallet,
g.IDMagazzino,
g.IDArea,
g.IDCella,
g.IDStato
FROM dbo.XMag_GiacenzaPallet AS g
WHERE g.IDCella <> 9999
AND g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
AND EXISTS (
SELECT 1
FROM SAMA1.dbo.LOTSER AS ls
INNER JOIN SAMA1.dbo.LOTTIBF AS lb
ON lb.IDLOTSER = ls.ID
INNER JOIN SAMA1.dbo.FATRIG AS fr
ON lb.IDFATRIG = fr.ID
INNER JOIN SAMA1.dbo.BAMTES AS bt
ON bt.ID = fr.IDBAM
WHERE bt.ANNDOC >= 2011
AND bt.STATO = 'D'
AND LEFT(ls.NUMSER, 6) COLLATE Latin1_General_CI_AS =
:pallet COLLATE Latin1_General_CI_AS
)
ORDER BY g.IDCella DESC;
"""
SQL_RESOLVE_PHYSICAL_CELL = """
DECLARE @raw int = TRY_CONVERT(int, :destination);
DECLARE @cell_id int =
@@ -110,6 +157,7 @@ SQL_LEGACY_MOVE = """
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @RC int = 0;
DECLARE @RefreshPickingReservation bit = :refresh_picking_reservation;
EXEC dbo.sp_xMagGestioneMagazziniPallet
@IDOperatore = :id_operatore,
@@ -118,7 +166,10 @@ EXEC dbo.sp_xMagGestioneMagazziniPallet
@NumeroCella = :numero_cella,
@RC = @RC OUTPUT;
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
IF @RefreshPickingReservation = 1
BEGIN
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
END;
SELECT
@RC AS RC,
@@ -127,6 +178,75 @@ SELECT
:numero_cella AS NumeroCella;
"""
SQL_SKIP_PICKING_PALLET = """
SET NOCOUNT ON;
IF NOT EXISTS (
SELECT 1
FROM dbo.py_BarcodePickingListSkip
WHERE Documento = :documento
AND Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
AND IDStato = :id_stato
AND Risolto = 0
)
BEGIN
INSERT INTO dbo.py_BarcodePickingListSkip (
Documento,
Pallet,
IDStato,
IDOperatore,
DataOra,
Motivo,
Risolto
)
VALUES (
:documento,
:pallet,
:id_stato,
:id_operatore,
SYSDATETIME(),
:motivo,
0
);
END;
SELECT
:documento AS Documento,
:pallet AS Pallet,
:id_stato AS IDStato;
"""
SQL_ACTIVE_SKIPPED_DOCUMENT = """
SELECT TOP (1)
s.Documento,
COUNT(*) AS SkippedCount
FROM dbo.py_BarcodePickingListSkip AS s
WHERE s.IDStato = :id_stato
AND s.Risolto = 0
AND EXISTS (
SELECT 1
FROM dbo.py_ViewPackingListRestante AS pl
WHERE CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS =
s.Documento COLLATE Latin1_General_CI_AS
AND pl.IDStato = :id_stato
)
GROUP BY s.Documento
ORDER BY MIN(s.DataOra);
"""
SQL_RELEASE_PICKING_DOCUMENT = """
SET NOCOUNT ON;
DECLARE @RC int = 0;
EXEC dbo.py_sp_xExePackingListPallet
@IDOperatore = :id_operatore,
@Documento = :documento,
@Azione = 'S',
@RC = @RC OUTPUT;
SELECT CAST(@RC AS int) AS RC;
"""
def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
"""Convert ``query_json`` payloads to a list of row dictionaries."""
@@ -204,6 +324,87 @@ class BarcodeRepository:
res = await self.db_client.query_json(SQL_OPEN_LOCATIONS_BY_PALLET, {"pallet": str(pallet or "").strip()})
return _rows_to_dicts(res)
async def fetch_open_pallets_by_cell(self, id_cella: int) -> list[dict[str, Any]]:
"""Return pallets currently present in a physical cell."""
res = await self.db_client.query_json(SQL_OPEN_PALLETS_BY_CELL, {"id_cella": int(id_cella)})
return _rows_to_dicts(res)
async def fetch_closed_picking_by_pallet(self, pallet: str) -> dict[str, Any] | None:
"""Return a closed picking-list row for a pallet already considered shipped."""
res = await self.db_client.query_json(SQL_CLOSED_PICKING_BY_PALLET, {"pallet": str(pallet or "").strip()})
rows = _rows_to_dicts(res)
return rows[0] if rows else None
async def skip_picking_pallet(
self,
*,
documento: str,
pallet: str,
id_stato: int,
operator_id: int,
motivo: str = "UDC non trovata dal magazziniere",
) -> None:
"""Persist one skipped picking-list pallet so the queue can advance."""
params = {
"documento": str(documento or "").strip(),
"pallet": str(pallet or "").strip(),
"id_stato": int(id_stato),
"id_operatore": int(operator_id),
"motivo": str(motivo or "").strip(),
}
log_runtime_event(
"Barcode WMS",
(
"PICKING SKIP START "
f"documento={params['documento']} "
f"pallet={params['pallet']} "
f"id_stato={params['id_stato']} "
f"operator={params['id_operatore']}"
),
)
await self.db_client.query_json(SQL_SKIP_PICKING_PALLET, params, commit=True)
log_runtime_event(
"Barcode WMS",
(
"PICKING SKIP OK "
f"documento={params['documento']} "
f"pallet={params['pallet']} "
f"id_stato={params['id_stato']}"
),
)
async def fetch_active_skipped_document(self, id_stato: int) -> dict[str, Any] | None:
"""Return a reserved document whose remaining pallets were all skipped."""
res = await self.db_client.query_json(SQL_ACTIVE_SKIPPED_DOCUMENT, {"id_stato": int(id_stato)})
rows = _rows_to_dicts(res)
return rows[0] if rows else None
async def release_picking_document(self, *, documento: str, operator_id: int) -> int:
"""Release a reserved picking-list document without closing its residual rows."""
params = {
"documento": str(documento or "").strip(),
"id_operatore": int(operator_id),
}
log_runtime_event(
"Barcode WMS",
f"PICKING AUTO RELEASE START documento={params['documento']} operator={params['id_operatore']}",
)
res = await self.db_client.query_json(SQL_RELEASE_PICKING_DOCUMENT, params, as_dict_rows=True, commit=True)
rows = _rows_to_dicts(res)
rc = 0
if rows:
try:
rc = int(rows[0].get("RC") or 0)
except Exception:
rc = 0
log_runtime_event("Barcode WMS", f"PICKING AUTO RELEASE END documento={params['documento']} rc={rc}")
return rc
async def resolve_physical_cell(self, destination: str) -> DestinationCell | None:
"""Accept either an internal cell ID or the scanned legacy cell barcode."""
@@ -226,6 +427,7 @@ class BarcodeRepository:
barcode_cella: str,
barcode_pallet: str,
numero_cella: int,
refresh_picking_reservation: bool = True,
) -> LegacyMoveResult:
"""Execute the same stored procedure used by the C# barcode form."""
@@ -234,6 +436,7 @@ class BarcodeRepository:
"barcode_cella": str(barcode_cella or "").strip(),
"barcode_pallet": str(barcode_pallet or "").strip(),
"numero_cella": int(numero_cella),
"refresh_picking_reservation": 1 if refresh_picking_reservation else 0,
}
log_runtime_event(
"Barcode WMS",
@@ -241,7 +444,8 @@ class BarcodeRepository:
"MOVE START "
f"operator={params['id_operatore']} "
f"cell={params['barcode_cella']} "
f"pallet={params['barcode_pallet']}"
f"pallet={params['barcode_pallet']} "
f"refresh_pl={params['refresh_picking_reservation']}"
),
)
try:

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Literal
from barcode_repository import BarcodeRepository, LegacyMoveResult
@@ -30,6 +30,7 @@ class BarcodeViewState:
scanned_pallet: str = ""
auto_advance_delay_ms: int = 0
destination_readonly: bool = False
focus_destination: bool = False
@dataclass
@@ -51,8 +52,8 @@ class BarcodeService:
LIGHT_GREEN = "#d9ead3"
GREEN_YELLOW = "#e2f0cb"
CONVENTIONAL_LOCATION_BY_CELL = {
1000: "5E1.1",
9999: "7G.1.1",
1000: "5E.1.1 - Non scaffalata",
9999: "7G.1.1 - Spedita",
}
def __init__(self, repository: BarcodeRepository, operator_id: int):
@@ -114,6 +115,53 @@ class BarcodeService:
)
return self._state
def resume_priority_state(self, state: BarcodeViewState) -> BarcodeViewState:
"""Restore the exact picking pallet that was visible before a local pause."""
if state.mode not in ("priority_high", "priority_low"):
return self._state
self._current_priority_state = 1 if state.mode == "priority_high" else 0
self._state = replace(state, scanned_pallet="", auto_advance_delay_ms=0)
return self._state
async def skip_current_picking_pallet(self) -> BarcodeActionResult:
"""Mark the currently proposed picking pallet as skipped and advance."""
mode = str(self._state.mode or "")
if mode not in ("priority_high", "priority_low"):
return BarcodeActionResult(False, self._state, "Nessuna picking list attiva.")
id_stato = 1 if mode == "priority_high" else 0
documento = str(self._state.document or "").strip()
pallet = str(self._state.expected_pallet or "").strip()
if not documento or not pallet:
self._state.status_text = "Nessuna UDC da saltare."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
await self.repository.skip_picking_pallet(
documento=documento,
pallet=pallet,
id_stato=id_stato,
operator_id=self.operator_id,
)
queue_label = "Alta priorita' (F1)" if id_stato == 1 else "Bassa priorita' (F2)"
self._current_priority_state = id_stato
self._state = BarcodeViewState(
mode="confirm",
queue_label=queue_label,
status_text="UDC saltata - chiusura manuale richiesta",
status_color=self.RED,
source_location=self._state.source_location,
document=documento,
customer="UDC non trovata",
expected_pallet=pallet,
destination_barcode=self.SHIPPED_BARCODE,
auto_advance_delay_ms=1500,
destination_readonly=True,
)
return BarcodeActionResult(True, self._state, self._state.status_text)
async def start_priority_queue(self, id_stato: int) -> BarcodeActionResult:
"""Load the next item of the selected legacy priority queue."""
@@ -121,6 +169,24 @@ class BarcodeService:
self._current_priority_state = int(id_stato)
queue_label = "Alta priorita' (F1)" if int(id_stato) == 1 else "Bassa priorita' (F2)"
if not row:
skipped_doc = await self.repository.fetch_active_skipped_document(int(id_stato))
if skipped_doc:
documento = str(skipped_doc.get("Documento") or "").strip()
skipped_count = int(skipped_doc.get("SkippedCount") or 0)
await self.repository.release_picking_document(
documento=documento,
operator_id=self.operator_id,
)
self._current_priority_state = -1
self._state = BarcodeViewState(
mode="manual_unload",
queue_label=queue_label,
status_text=f"PL {documento} sospesa: {skipped_count} UDC saltate. Riprenota per completare.",
status_color=self.RED,
destination_barcode=self.NON_SCAFFALATA_BARCODE,
destination_readonly=False,
)
return BarcodeActionResult(True, self._state)
self._current_priority_state = -1
self._state = BarcodeViewState(
mode="manual_unload",
@@ -176,13 +242,92 @@ class BarcodeService:
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
if not is_picking_unload:
blocked = await self._block_closed_picking_free_move(pallet, destination)
if blocked is not None:
return blocked
target_barcode = destination
target_numero_cella = int(destination)
target_id_cella = 9999 if destination == self.SHIPPED_BARCODE else (1000 if destination == self.NON_SCAFFALATA_BARCODE else None)
target_display = destination
if is_direct_load:
resolved_cell = await self.repository.resolve_physical_cell(destination)
if not resolved_cell:
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = f"Cella non valida: {destination}."
self._state.status_color = self.RED
self._state.focus_destination = True
return BarcodeActionResult(False, self._state, self._state.status_text)
occupants = await self.repository.fetch_open_pallets_by_cell(resolved_cell.id_cella)
if occupants:
log_runtime_event(
"Barcode WMS",
(
"MOVE BLOCKED DESTINATION_OCCUPIED "
f"pallet={pallet} "
f"cell={resolved_cell.id_cella} "
f"occupants={','.join(str(row.get('BarcodePallet') or '') for row in occupants)}"
),
)
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = "Errore: cella scelta occupata, cambiare cella di destinazione."
self._state.status_color = self.RED
self._state.destination_readonly = False
self._state.focus_destination = True
return BarcodeActionResult(False, self._state, self._state.status_text)
target_barcode = resolved_cell.barcode_cella
target_numero_cella = resolved_cell.numero_cella
target_id_cella = resolved_cell.id_cella
target_display = resolved_cell.ubicazione or destination
current_location = await self.repository.fetch_current_location_by_pallet(pallet)
picking_before_move = await self.repository.fetch_picking_by_pallet(pallet)
picking_before_move = (
None
if current_location
else await self.repository.fetch_picking_by_pallet(pallet)
)
if not current_location and not picking_before_move:
self._state.scanned_pallet = pallet
self._state.status_text = "UDC non presente a magazzino."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
if is_direct_load:
trace_row = await self.repository.fetch_trace_by_pallet(pallet)
if not trace_row:
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = "Errore: UDC non riconosciuta in ERP/SAM."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
log_runtime_event(
"Barcode WMS",
f"AUTO INTAKE NON_SHELVED pallet={pallet} before_cell={target_id_cella}",
)
await self.repository.execute_legacy_move(
operator_id=self.operator_id,
barcode_cella=self.NON_SCAFFALATA_BARCODE,
barcode_pallet=pallet,
numero_cella=int(self.NON_SCAFFALATA_BARCODE),
refresh_picking_reservation=False,
)
current_location = await self.repository.fetch_current_location_by_pallet(pallet)
try:
current_id_cella = int((current_location or {}).get("IDCella") or 0)
except Exception:
current_id_cella = 0
if current_id_cella != 1000:
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = "Movimento non confermato: ingresso UDC non scaffalata non riuscito."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
else:
self._state.scanned_pallet = pallet
self._state.status_text = "UDC non presente a magazzino."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
open_locations = await self.repository.fetch_open_locations_by_pallet(pallet)
distinct_cells = sorted({
@@ -205,16 +350,13 @@ class BarcodeService:
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
target_barcode = destination
target_numero_cella = int(destination)
target_id_cella = 9999 if destination == self.SHIPPED_BARCODE else (1000 if destination == self.NON_SCAFFALATA_BARCODE else None)
target_display = destination
if destination not in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE):
if not is_direct_load and destination not in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE):
resolved_cell = await self.repository.resolve_physical_cell(destination)
if not resolved_cell:
self._state.scanned_pallet = pallet
self._state.status_text = f"Cella non valida: {destination}."
self._state.status_color = self.RED
self._state.focus_destination = True
return BarcodeActionResult(False, self._state, self._state.status_text)
target_barcode = resolved_cell.barcode_cella
target_numero_cella = resolved_cell.numero_cella
@@ -226,6 +368,7 @@ class BarcodeService:
barcode_cella=target_barcode,
barcode_pallet=pallet,
numero_cella=target_numero_cella,
refresh_picking_reservation=is_picking_unload,
)
final_location = await self.repository.fetch_current_location_by_pallet(pallet)
@@ -246,7 +389,7 @@ class BarcodeService:
destination_barcode=destination,
destination_display=target_display,
last_priority_state=self._current_priority_state,
auto_advance_delay_ms=5000 if (is_direct_unload or is_direct_load) else 3000 if is_picking_unload else 0,
auto_advance_delay_ms=3000 if is_picking_unload else 0,
)
except Exception as exc:
log_exception("Barcode WMS", exc, context=f"post move state pallet={pallet} destination={destination}")
@@ -272,6 +415,10 @@ class BarcodeService:
if not source.isdigit():
return BarcodeActionResult(False, self._state, "La cella da scaricare deve essere numerica.")
blocked = await self._block_closed_picking_free_move(pallet, source)
if blocked is not None:
return blocked
current_location = await self.repository.fetch_current_location_by_pallet(pallet)
if not current_location:
self._state.scanned_pallet = pallet
@@ -344,7 +491,149 @@ class BarcodeService:
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
return await self.submit(scanned_pallet=pallet, destination_barcode=self.NON_SCAFFALATA_BARCODE)
await self.repository.execute_legacy_move(
operator_id=self.operator_id,
barcode_cella=self.NON_SCAFFALATA_BARCODE,
barcode_pallet=pallet,
numero_cella=int(self.NON_SCAFFALATA_BARCODE),
refresh_picking_reservation=False,
)
final_location = await self.repository.fetch_current_location_by_pallet(pallet)
try:
final_cell = int((final_location or {}).get("IDCella") or 0)
except Exception:
final_cell = 0
if final_cell != 1000:
self._state.scanned_pallet = pallet
self._state.destination_barcode = source
self._state.status_text = "Movimento non confermato: la UDC non risulta non scaffalata."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
try:
self._state = await self._build_post_move_state(
barcode_pallet=pallet,
destination_barcode=self.NON_SCAFFALATA_BARCODE,
destination_display=self.CONVENTIONAL_LOCATION_BY_CELL[1000],
last_priority_state=self._current_priority_state,
auto_advance_delay_ms=0,
)
except Exception as exc:
log_exception("Barcode WMS", exc, context=f"post source-check unload pallet={pallet} source={source}")
self._state = BarcodeViewState(
mode="confirm",
queue_label="Conferma movimento",
status_text="Movimento eseguito. Dettagli non aggiornati.",
status_color=self.GREEN_YELLOW,
destination_barcode=self.NON_SCAFFALATA_BARCODE,
scanned_pallet=pallet,
)
return BarcodeActionResult(True, self._state, self._state.status_text)
async def _block_closed_picking_free_move(
self,
pallet: str,
destination_barcode: str,
) -> BarcodeActionResult | None:
closed_row = await self.repository.fetch_closed_picking_by_pallet(pallet)
if not closed_row:
return None
log_runtime_event(
"Barcode WMS",
(
"MOVE BLOCKED CLOSED_PICKING "
f"pallet={pallet} "
f"destination={destination_barcode} "
f"id_cella={closed_row.get('IDCella')} "
f"id_stato={closed_row.get('IDStato')}"
),
)
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination_barcode
self._state.status_text = "Errore: UDC gia' spedita, problema nella locazione o nella UDC."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
async def reconcile_after_submit_exception(
self,
*,
scanned_pallet: str,
destination_barcode: str,
) -> BarcodeActionResult:
"""Check whether a failed client round-trip actually committed on SQL Server."""
pallet = str(scanned_pallet or "").strip()
destination = str(destination_barcode or "").strip()
if not pallet or not destination or not destination.isdigit():
self._state.status_text = "Transazione non completata, ripeti l'operazione."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
target_id_cella = 9999 if destination == self.SHIPPED_BARCODE else (1000 if destination == self.NON_SCAFFALATA_BARCODE else None)
target_display = destination
if target_id_cella is None:
resolved_cell = await self.repository.resolve_physical_cell(destination)
if not resolved_cell:
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = "Transazione non completata, ripeti l'operazione."
self._state.status_color = self.RED
self._state.focus_destination = True
return BarcodeActionResult(False, self._state, self._state.status_text)
target_id_cella = int(resolved_cell.id_cella)
target_display = resolved_cell.ubicazione or destination
current_location = await self.repository.fetch_current_location_by_pallet(pallet)
try:
current_id_cella = int((current_location or {}).get("IDCella") or 0)
except Exception:
current_id_cella = 0
if current_id_cella != target_id_cella:
log_runtime_event(
"Barcode WMS",
(
"MOVE RECOVERY NOT_CONFIRMED "
f"pallet={pallet} "
f"destination={destination} "
f"expected_cell={target_id_cella} "
f"actual_cell={current_id_cella}"
),
)
self._state.scanned_pallet = pallet
self._state.destination_barcode = destination
self._state.status_text = "Transazione non completata, ripeti l'operazione."
self._state.status_color = self.RED
return BarcodeActionResult(False, self._state, self._state.status_text)
last_priority_state = self._current_priority_state
is_recovered_picking = bool(destination == self.SHIPPED_BARCODE and last_priority_state in (0, 1))
auto_advance_delay_ms = 3000 if is_recovered_picking else 0
state = await self._build_post_move_state(
barcode_pallet=pallet,
destination_barcode=destination,
destination_display=target_display,
last_priority_state=last_priority_state,
auto_advance_delay_ms=auto_advance_delay_ms,
)
state.status_text = (
"Ok Scarico - risposta persa recuperata"
if is_recovered_picking
else "Movimento completato - risposta persa recuperata"
)
state.status_color = self.GREEN_YELLOW
self._state = state
log_runtime_event(
"Barcode WMS",
(
"MOVE RECOVERY CONFIRMED "
f"pallet={pallet} "
f"destination={destination} "
f"cell={target_id_cella}"
),
)
return BarcodeActionResult(True, self._state, self._state.status_text)
async def _build_post_move_state(
self,
@@ -357,7 +646,15 @@ class BarcodeService:
) -> BarcodeViewState:
"""Mirror the legacy confirmation flow after one stored-procedure move."""
picking_row = await self.repository.fetch_picking_by_pallet(barcode_pallet)
is_priority_shipping_confirmation = (
destination_barcode == self.SHIPPED_BARCODE
and last_priority_state in (0, 1)
)
picking_row = (
await self.repository.fetch_picking_by_pallet(barcode_pallet)
if is_priority_shipping_confirmation
else None
)
if picking_row:
customer = f"{picking_row.get('CodNazione') or ''} - {picking_row.get('NAZIONE') or ''}".strip(" -")
source_location = self._display_location(
@@ -384,14 +681,20 @@ class BarcodeService:
prodotto = str(trace_row.get("Prodotto") or "")
descrizione = str(trace_row.get("Descrizione") or "")
queue_label = "Alta priorita' (F1)" if last_priority_state == 1 else ("Bassa priorita' (F2)" if last_priority_state == 0 else "Conferma movimento")
destination_label = self._display_destination_location(
destination_barcode=destination_barcode,
destination_display=destination_display,
)
return BarcodeViewState(
mode="confirm",
queue_label=queue_label,
status_text=(
"Ok Scarico" if destination_barcode in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE) else f"Ok Carico - {destination_display}"
f"Ok Scarico - {destination_label}"
if destination_barcode in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE)
else f"Ok Carico - {destination_label}"
),
status_color=self.GREEN_YELLOW,
source_location=str(destination_display or destination_barcode or ""),
source_location=destination_label,
document=(
self.CONVENTIONAL_LOCATION_BY_CELL[9999 if destination_barcode == self.SHIPPED_BARCODE else 1000]
if destination_barcode in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE) and last_priority_state in (0, 1)
@@ -422,6 +725,15 @@ class BarcodeService:
scanned_pallet=barcode_pallet,
)
def _display_destination_location(self, *, destination_barcode: str, destination_display: str) -> str:
"""Return the confirmation label for a movement destination."""
if destination_barcode == self.NON_SCAFFALATA_BARCODE:
return self.CONVENTIONAL_LOCATION_BY_CELL[1000]
if destination_barcode == self.SHIPPED_BARCODE:
return self.CONVENTIONAL_LOCATION_BY_CELL[9999]
return str(destination_display or destination_barcode or "")
def _display_location(self, *, cella: object, ubicazione: object) -> str:
"""Return the operator-facing location, honoring legacy conventional cells."""

View File

@@ -26,6 +26,11 @@ DEFAULT_DB_CONFIG: dict[str, Any] = {
"driver": "ODBC Driver 17 for SQL Server",
"trust_server_certificate": True,
"encrypt": "",
"query_profiler": {
"enabled": False,
"slow_ms": 0,
"sql_limit": 4000,
},
}

543
diagnostica.py Normal file
View File

@@ -0,0 +1,543 @@
"""Operational diagnostic checks for warehouse data quadrature."""
from __future__ import annotations
import tkinter as tk
from datetime import datetime
from tkinter import filedialog, 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 runtime_support import log_exception
from ui_tables import style_treeview, zebra_tag
from ui_theme import theme_color, theme_font, theme_section, theme_value
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
SQL_SHIPPED_NOT_DISASSOCIATED = """
SELECT DISTINCT
LTRIM(RTRIM(g.BarcodePallet)) AS UDC,
CAST('Spedita ma non disassociata' AS varchar(64)) AS Diagnostica,
CONCAT(
RTRIM(c.Corsia),
' - ',
RTRIM(CAST(c.Colonna AS varchar(32))),
' - ',
RTRIM(CAST(c.Fila AS varchar(32)))
) AS NomeSimbolicoCella
FROM dbo.XMag_GiacenzaPallet AS g
INNER JOIN dbo.Celle AS c
ON c.ID = g.IDCella
INNER JOIN dbo.py_XMag_ViewPackingListStorico AS chiuse
ON LTRIM(RTRIM(chiuse.Pallet)) COLLATE Latin1_General_CI_AS =
LTRIM(RTRIM(g.BarcodePallet)) COLLATE Latin1_General_CI_AS
WHERE chiuse.StatoDocumento = 'D'
AND g.IDCella <> 9999
ORDER BY NomeSimbolicoCella, UDC;
"""
SQL_MULTIPLE_UDC_BY_CELL = """
WITH celle_multiple AS (
SELECT
g.IDCella
FROM dbo.XMag_GiacenzaPallet AS g
WHERE g.IDCella NOT IN (1000, 9999)
GROUP BY g.IDCella
HAVING COUNT(DISTINCT LTRIM(RTRIM(g.BarcodePallet))) > 1
)
SELECT
CONCAT(
RTRIM(c.Corsia),
' - ',
RTRIM(CAST(c.Colonna AS varchar(32))),
' - ',
RTRIM(CAST(c.Fila AS varchar(32)))
) AS CELLA,
LTRIM(RTRIM(g.BarcodePallet)) AS UDC
FROM dbo.XMag_GiacenzaPallet AS g
INNER JOIN celle_multiple AS cm
ON cm.IDCella = g.IDCella
INNER JOIN dbo.Celle AS c
ON c.ID = g.IDCella
ORDER BY CELLA, UDC;
"""
SQL_SAM_UDC_NOT_IN_WMS = """
WITH recent_lotser AS (
SELECT TOP (5000)
ls.ID,
LEFT(ls.NUMSER, 6) AS UDC,
ls.NUMLOT AS Lotto,
ls.IDARTICO
FROM SAMA1.dbo.LOTSER AS ls
WHERE NULLIF(LTRIM(RTRIM(LEFT(ls.NUMSER, 6))), '') IS NOT NULL
AND TRY_CONVERT(int, LTRIM(RTRIM(LEFT(ls.NUMSER, 6)))) > 0
AND LEN(LTRIM(RTRIM(LEFT(ls.NUMSER, 6)))) = 6
AND ls.NUMLOT LIKE CONCAT('P', RIGHT(CONVERT(varchar(4), YEAR(GETDATE())), 2), '%')
ORDER BY ls.ID DESC
), sam_udc AS (
SELECT
LTRIM(RTRIM(r.UDC)) AS UDC,
MIN(r.Lotto) AS Lotto,
MIN(a.CODICE) AS Prodotto,
MIN(a.DESCR) AS Descrizione,
MAX(r.ID) AS UltimoIDLotSer
FROM recent_lotser AS r
INNER JOIN SAMA1.dbo.ARTICO AS a
ON a.ID = r.IDARTICO
GROUP BY LTRIM(RTRIM(r.UDC))
)
SELECT TOP (100)
s.UDC,
s.Lotto,
s.Prodotto,
s.Descrizione
FROM sam_udc AS s
WHERE NOT EXISTS (
SELECT 1
FROM dbo.MagazziniPallet AS mp
WHERE LTRIM(RTRIM(mp.Attributo)) COLLATE Latin1_General_CI_AS =
s.UDC COLLATE Latin1_General_CI_AS
)
ORDER BY s.UltimoIDLotSer DESC;
"""
SHIPPED_NOT_DISASSOCIATED_COLUMNS = (
("UDC", "diagnostics.col.udc", "UDC", 130, "w"),
(
"Diagnostica",
"diagnostics.col.shipped_not_disassociated",
"Spedita ma non disassociata",
280,
"w",
),
("NomeSimbolicoCella", "diagnostics.col.symbolic_cell", "Nome simbolico cella", 240, "w"),
)
MULTIPLE_UDC_COLUMNS = (
("CELLA", "diagnostics.col.cell", "CELLA", 220, "w"),
("UDC", "diagnostics.col.udc", "UDC", 160, "w"),
)
SAM_UDC_NOT_IN_WMS_COLUMNS = (
("UDC", "diagnostics.col.udc", "UDC", 130, "w"),
("Lotto", "diagnostics.col.lot", "Lotto", 160, "w"),
("Prodotto", "diagnostics.col.product", "Prodotto", 150, "w"),
("Descrizione", "diagnostics.col.description", "Descrizione", 360, "w"),
)
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 DiagnosticaWindow(ctk.CTkToplevel):
"""Window collecting read-only diagnostic queries."""
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("diagnostics_window", theme_section("search_window", {}))
self._locale_catalog = load_locale_catalog()
self._async = AsyncRunner(self)
self._busy = InlineBusyOverlay(self, self._theme)
self._load_in_progress = False
self._current_check = ""
self.var_status = tk.StringVar(value="")
self._last_export_slug = "diagnostica"
self._current_columns = SHIPPED_NOT_DISASSOCIATED_COLUMNS
self._column_titles: dict[str, str] = {}
self._sort_column = ""
self._sort_descending = False
self.title(
versioned_title(
loc_text("diagnostics.title", catalog=self._locale_catalog, default="Diagnostica"),
__name__,
)
)
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x680")))
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()
def _is_alive(self) -> bool:
try:
return bool(self.winfo_exists())
except tk.TclError:
return False
def _hide_busy_safe(self) -> None:
try:
self._busy.hide()
except Exception as exc:
log_exception(__name__, exc, context="hide busy diagnostica")
def _build_ui(self) -> None:
self.grid_rowconfigure(2, 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)
button_font = theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))
ctk.CTkButton(
top,
text=loc_text(
"diagnostics.button.shipped_not_disassociated",
catalog=self._locale_catalog,
default="UDC spedite non disassociate",
),
command=self._load_shipped_not_disassociated,
font=button_font,
).grid(row=0, column=0, sticky="w", padx=(0, 8))
ctk.CTkButton(
top,
text=loc_text(
"diagnostics.button.multiple_udc_by_cell",
catalog=self._locale_catalog,
default="Celle con multiple UDC",
),
command=self._load_multiple_udc_by_cell,
font=button_font,
).grid(row=0, column=1, sticky="w", padx=(0, 8))
ctk.CTkButton(
top,
text=loc_text(
"diagnostics.button.sam_udc_not_in_wms",
catalog=self._locale_catalog,
default="Ultime UDC SAM non in WMS",
),
command=self._load_sam_udc_not_in_wms,
font=button_font,
).grid(row=0, column=2, sticky="w", padx=(0, 8))
self.btn_export = ctk.CTkButton(
top,
text=loc_text("diagnostics.button.export", catalog=self._locale_catalog, default="Esporta XLSX"),
command=self._export_xlsx,
font=button_font,
state="disabled",
)
self.btn_export.grid(row=0, column=4, sticky="e")
ctk.CTkLabel(
self,
textvariable=self.var_status,
anchor="w",
font=theme_font(self._theme, "status_font", ("Segoe UI", 10, "bold")),
).grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 6))
wrap = ctk.CTkFrame(self)
wrap.grid(row=2, column=0, sticky="nsew", padx=8, pady=(0, 8))
wrap.grid_rowconfigure(0, weight=1)
wrap.grid_columnconfigure(0, weight=1)
self.tree = ttk.Treeview(wrap, show="headings")
self._configure_tree_columns(self._current_columns)
style_treeview(
self.tree,
style_name="Diagnostics.Treeview",
rowheight=22,
font=("", 9),
heading_font=("", 9, "bold"),
)
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 _set_status(self, text: str) -> None:
self.var_status.set(text)
def _configure_tree_columns(self, columns) -> None:
self.tree.delete(*self.tree.get_children(""))
keys = tuple(str(col[0]) for col in columns)
self.tree.configure(columns=keys, show="headings")
self._sort_column = ""
self._sort_descending = False
self._column_titles = {}
for key, locale_key, default, width, anchor in columns:
title = loc_text(str(locale_key), catalog=self._locale_catalog, default=str(default))
self._column_titles[str(key)] = title
self.tree.heading(
key,
text=title,
command=lambda col=str(key): self._sort_by_column(col),
)
self.tree.column(key, width=int(width), anchor=str(anchor), stretch=True)
def _refresh_sort_headings(self) -> None:
for key, title in self._column_titles.items():
suffix = ""
if key == self._sort_column:
suffix = " v" if self._sort_descending else " ^"
self.tree.heading(key, text=f"{title}{suffix}", command=lambda col=key: self._sort_by_column(col))
def _sort_key(self, value: Any) -> tuple[int, Any]:
text = str(value or "").strip()
if not text:
return (2, "")
normalized = text.replace(".", "").replace(",", ".")
try:
return (0, float(normalized))
except ValueError:
return (1, text.casefold())
def _sort_by_column(self, column: str) -> None:
columns = list(self.tree["columns"])
if column not in columns:
return
descending = not self._sort_descending if self._sort_column == column else False
col_index = columns.index(column)
rows = []
for iid in self.tree.get_children(""):
values = self.tree.item(iid, "values")
value = values[col_index] if col_index < len(values) else ""
rows.append((self._sort_key(value), iid))
rows.sort(key=lambda item: item[0], reverse=descending)
for index, (_key, iid) in enumerate(rows):
self.tree.move(iid, "", index)
self.tree.item(iid, tags=(zebra_tag(index),))
self._sort_column = column
self._sort_descending = descending
self._refresh_sort_headings()
def _load_shipped_not_disassociated(self) -> None:
self._run_check(
key="shipped_not_disassociated",
sql=SQL_SHIPPED_NOT_DISASSOCIATED,
columns=SHIPPED_NOT_DISASSOCIATED_COLUMNS,
busy_text=loc_text(
"diagnostics.busy.shipped_not_disassociated",
catalog=self._locale_catalog,
default="Cerco UDC spedite ma ancora in cella...",
),
)
def _load_multiple_udc_by_cell(self) -> None:
self._run_check(
key="multiple_udc_by_cell",
sql=SQL_MULTIPLE_UDC_BY_CELL,
columns=MULTIPLE_UDC_COLUMNS,
busy_text=loc_text(
"diagnostics.busy.multiple_udc_by_cell",
catalog=self._locale_catalog,
default="Cerco celle con multiple UDC...",
),
)
def _load_sam_udc_not_in_wms(self) -> None:
self._run_check(
key="sam_udc_not_in_wms",
sql=SQL_SAM_UDC_NOT_IN_WMS,
columns=SAM_UDC_NOT_IN_WMS_COLUMNS,
busy_text=loc_text(
"diagnostics.busy.sam_udc_not_in_wms",
catalog=self._locale_catalog,
default="Cerco le ultime UDC presenti in SAM ma non ancora in WMS...",
),
)
def _run_check(self, *, key: str, sql: str, columns, busy_text: str) -> None:
if self._load_in_progress or not self._is_alive():
return
async def job():
return await self.db_client.query_json(sql, as_dict_rows=True)
self._load_in_progress = True
self._current_check = key
self._last_export_slug = key
self._current_columns = columns
self._configure_tree_columns(columns)
try:
self.btn_export.configure(state="disabled")
except Exception:
pass
try:
self._busy.show(busy_text)
self._async.run(job(), self._on_loaded, self._on_error)
except Exception as exc:
self._load_in_progress = False
self._hide_busy_safe()
log_exception(__name__, exc, context=f"avvio diagnostica {key}")
messagebox.showerror(
loc_text("diagnostics.title", catalog=self._locale_catalog, default="Diagnostica"),
str(exc),
parent=self if self._is_alive() else None,
)
def _on_loaded(self, res: dict[str, Any] | None) -> None:
self._load_in_progress = False
self._hide_busy_safe()
if not self._is_alive():
return
try:
rows = _rows_to_dicts(res)
self.tree.delete(*self.tree.get_children(""))
keys = [str(col[0]) for col in self._current_columns]
for index, row in enumerate(rows):
self.tree.insert(
"",
"end",
values=tuple(row.get(key) or "" for key in keys),
tags=(zebra_tag(index),),
)
try:
self.btn_export.configure(state="normal" if rows else "disabled")
except Exception:
pass
self._set_status(
loc_text(
"diagnostics.status.rows",
catalog=self._locale_catalog,
default="Righe trovate: {count}",
).format(count=len(rows))
)
except Exception as exc:
log_exception(__name__, exc, context="render diagnostica")
messagebox.showerror(
loc_text("diagnostics.title", catalog=self._locale_catalog, default="Diagnostica"),
str(exc),
parent=self,
)
def _on_error(self, exc: BaseException) -> None:
self._load_in_progress = False
self._hide_busy_safe()
log_exception(__name__, exc, context=f"query diagnostica {self._current_check}")
if not self._is_alive():
return
messagebox.showerror(
loc_text("diagnostics.title", catalog=self._locale_catalog, default="Diagnostica"),
str(exc),
parent=self,
)
def _export_xlsx(self) -> None:
"""Export the currently visible diagnostic grid to an Excel file."""
rows = [self.tree.item(iid, "values") for iid in self.tree.get_children("")]
if not rows:
messagebox.showinfo(
loc_text("diagnostics.export.title", catalog=self._locale_catalog, default="Esporta"),
loc_text(
"diagnostics.export.empty",
catalog=self._locale_catalog,
default="Non ci sono righe da esportare.",
),
parent=self,
)
return
try:
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter
except Exception as exc:
messagebox.showerror(
loc_text("diagnostics.export.title", catalog=self._locale_catalog, default="Esporta"),
loc_text(
"diagnostics.export.dep",
catalog=self._locale_catalog,
default="Per l'esportazione serve 'openpyxl' (pip install openpyxl).",
).format(error=exc),
parent=self,
)
return
ts = datetime.now().strftime("%Y%m%d_%H%M")
default_name = f"{self._last_export_slug}_{ts}.xlsx"
path = filedialog.asksaveasfilename(
parent=self,
title=loc_text("diagnostics.export.title", catalog=self._locale_catalog, default="Esporta"),
defaultextension=".xlsx",
filetypes=[("Excel Workbook", "*.xlsx")],
initialfile=default_name,
)
if not path:
return
try:
wb = Workbook()
ws = wb.active
ws.title = "Diagnostica"
columns = list(self.tree["columns"])
headers = [str(self.tree.heading(col, "text") or col) for col in columns]
for col_index, header in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_index, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center")
for row_index, row in enumerate(rows, start=2):
for col_index, value in enumerate(row, start=1):
ws.cell(row=row_index, column=col_index, value=value)
widths: dict[int, int] = {}
for row in ws.iter_rows(values_only=True):
for col_index, value in enumerate(row, start=1):
widths[col_index] = max(widths.get(col_index, 0), len("" if value is None else str(value)))
for col_index, width in widths.items():
ws.column_dimensions[get_column_letter(col_index)].width = min(max(width + 2, 12), 70)
wb.save(path)
messagebox.showinfo(
loc_text("diagnostics.export.title", catalog=self._locale_catalog, default="Esporta"),
loc_text("diagnostics.export.ok", catalog=self._locale_catalog, default="File creato:\n{path}").format(path=path),
parent=self,
)
except Exception as exc:
log_exception(__name__, exc, context="export diagnostica xlsx")
messagebox.showerror(
loc_text("diagnostics.export.title", catalog=self._locale_catalog, default="Esporta"),
loc_text(
"diagnostics.export.error",
catalog=self._locale_catalog,
default="Errore durante l'esportazione:\n{error}",
).format(error=exc),
parent=self,
)
def open_diagnostica_window(parent: tk.Misc, db_client, session=None) -> tk.Misc:
"""Open the diagnostics window."""
win = DiagnosticaWindow(parent, db_client, session=session)
place_window_fullsize_below_parent_later(parent, win)
win.bind("<Escape>", lambda _e: win.destroy())
return win

View File

@@ -0,0 +1,235 @@
# Diagnostica Scarico Celle Multiple
## Contesto
La finestra `Scarico` aperta da `Gestione layout` serve a gestire celle fisiche che contengono piu' UDC.
Nel database legacy non esisteva un vincolo rigido `una cella = una UDC`, quindi una cella rossa puo' contenere:
- UDC realmente compresenti nella stessa cella;
- UDC rimaste in cella anche se appartenenti a picking list gia' chiuse/spedite.
L'obiettivo della diagnostica non e' ricostruire tutta la storia della UDC, ma aiutare l'operatore a capire quali righe scaricare come spedite.
## Fonti Dati
La griglia della dialog parte dalla giacenza corrente:
```sql
dbo.XMag_GiacenzaPallet
```
Questa vista indica quali UDC risultano presenti nella cella aperta dal layout.
La diagnostica controlla anche:
```sql
dbo.XMag_GiacenzaPalletPlistChiuse
```
Questa vista non e' uno storico documentale puro. E' una vista ibrida che incrocia la giacenza corrente con i documenti SAM chiusi.
In pratica serve a individuare UDC ancora presenti in una cella fisica, ma appartenenti a una picking list/documento gia' chiuso.
## Conclusione Operativa
Per una cella fisica aperta da `Gestione layout`, i casi realmente utili sono solo due.
### 1. Nessuna Diagnostica
La UDC e' presente nella cella multipla e non compare in `XMag_GiacenzaPalletPlistChiuse`.
Significato:
```text
UDC normalmente presente in cella.
```
Non viene mostrato nessun messaggio nella colonna diagnostica.
### 2. Spedita Ma Non Scaricata Dalla Cella
La UDC e' presente nella cella fisica aperta, ma compare anche in `XMag_GiacenzaPalletPlistChiuse`.
Diagnostica mostrata:
```text
Motivo: spedita ma non scaricata dalla cella
```
Significato:
```text
La UDC appartiene a una picking list chiusa, ma non e' stata scaricata dalla cella fisica.
```
Operativamente questa UDC va selezionata e scaricata come spedita, quindi verso la cella virtuale:
```text
7G.1.1 / IDCella 9999 / barcode cella 9000000
```
## Casi Teorici Scartati Dalla Dialog
Durante l'analisi erano stati ipotizzati altri casi, ma sono stati esclusi dalla diagnostica della dialog per non confondere l'operatore.
### UDC Gia' In 7G.1.1
Se una UDC risulta davvero in `IDCella = 9999`, normalmente non dovrebbe comparire nella cella fisica aperta dal layout.
Se accadesse, sarebbe un'incoerenza tra viste o dati non sincronizzati.
Per questo la dialog di scarico da layout fisico non mostra piu' il messaggio:
```text
Motivo: spedita
```
### UDC In Picking List Chiusa Ma Non Spedita
Questo caso e' teoricamente possibile ma raro e ambiguo nel modello dati attuale.
Le viste disponibili non forniscono una distinzione documentale/storica abbastanza pulita da renderlo utile in questa dialog operativa.
Per questo la dialog non mostra piu':
```text
Motivo: in plist chiusa, non spedita e non scaricata dalla cella
```
Eventuali analisi piu' profonde vanno fatte nelle finestre di storico, non nella dialog rapida di scarico.
## Regola Finale Implementata
La diagnostica della dialog `Scarico` e' volutamente semplice:
```text
se UDC in XMag_GiacenzaPalletPlistChiuse:
"Motivo: spedita ma non scaricata dalla cella"
altrimenti:
diagnostica vuota
```
Questa scelta riduce il rischio di falsi positivi e mantiene chiaro il comportamento per l'operatore.
## Aggiornamento 04/07/2026 - Barcode Picking List
Durante i test sul barcode sono state introdotte alcune modifiche operative collegate alla gestione delle picking list, agli skip UDC e alla ripresa del lavoro dopo movimenti liberi.
### Anti Autosubmit Su Cambio Lista
Problema osservato:
```text
passando rapidamente da una picking list prenotata a una non prenotata,
il barcode poteva considerare come lettura valida un valore gia' presente nel campo Pallet
e scaricare automaticamente una UDC della nuova lista.
```
Correzione implementata:
```text
lo scarico automatico in picking list parte solo se il campo Pallet e' stato modificato
dall'operatore dopo il caricamento della UDC proposta.
```
In pratica, quando il programma carica una nuova UDC attesa, il valore visualizzato non puo' piu' generare da solo lo scarico. Serve una nuova lettura o digitazione effettiva.
### Skip UDC In Picking List
Durante l'evasione di una picking list, il tasto F4 assume il significato:
```text
[F4] Salta UDC
```
La UDC saltata viene registrata nella tabella:
```sql
dbo.py_BarcodePickingListSkip
```
Le righe aperte hanno:
```text
Risolto = 0
```
La query del barcode esclude le UDC saltate aperte, quindi la lista puo' proseguire sulle UDC successive senza chiudere definitivamente quelle non trovate.
### Fine Lista Con UDC Saltate
Quando una picking list non ha piu' UDC immediatamente lavorabili, ma contiene una o piu' UDC saltate, il barcode:
```text
non chiude la picking list;
rilascia la prenotazione;
lascia la lista riprenotabile;
mostra un messaggio di sospensione con numero UDC saltate.
```
La lista resta quindi aperta e potra' essere ripresa in un secondo momento.
### Riprenotazione E Ripresa UDC Saltate
Quando la picking list viene riprenotata, gli skip aperti del documento vengono marcati come risolti:
```sql
UPDATE dbo.py_BarcodePickingListSkip
SET Risolto = 1,
RisoltoDa = <operatore>,
RisoltoDataOra = SYSDATETIME()
WHERE Documento = <documento>
AND Risolto = 0;
```
Questo permette al barcode di riproporre le UDC saltate rimaste da evadere.
### Posizione UDC Dopo Movimenti Liberi
Una UDC saltata puo' essere movimentata in pausa tramite operazioni libere, ad esempio:
```text
versamento in una nuova cella fisica;
dissociazione verso 9001000 / Non scaffalata.
```
Alla ripresa della picking list, la posizione mostrata dal barcode riflette la giacenza corrente, perche' la vista operativa deriva `Cella` e `Ubicazione` da:
```sql
dbo.XMag_GiacenzaPallet
```
Quindi una UDC prima saltata e poi spostata verra' proposta con la nuova cella, oppure come:
```text
Non scaffalata
```
se e' stata dissociata verso la locazione virtuale `9001000`.
### Regola Conservata
Resta confermata la regola operativa:
```text
una UDC diventa Spedita / 7G.1.1 / 9000000 solo nel flusso picking list.
```
Le operazioni libere non spediscono una UDC. Possono solo versarla in una cella fisica oppure metterla in `9001000 / Non scaffalata`.
### File Coinvolti
Le modifiche principali sono nei moduli:
```text
barcode_client.py
barcode_repository.py
barcode_service.py
prenota_sprenota_sql.py
version_info.py
```
La patch DB cumulativa da usare sul database online e':
```text
apply_online_python_wms_full_patch.sql
```
La patch contiene gli oggetti SQL necessari al ramo Python e puo' essere eseguita una sola volta da SSMS.

View File

@@ -607,6 +607,7 @@ class LayoutWindow(ctk.CTkToplevel):
)
m.add_separator()
m.add_command(label="Copia ubicazione", command=lambda: self._copy(label))
m.add_command(label="Codice cella", command=lambda: self._show_cell_code(r, c))
x = self.winfo_pointerx() if event is None else event.x_root
y = self.winfo_pointery() if event is None else event.y_root
m.tk_popup(x, y)
@@ -648,6 +649,25 @@ class LayoutWindow(ctk.CTkToplevel):
except Exception:
return 0
def _show_cell_code(self, r: int, c: int) -> None:
"""Show the internal cell code for the selected layout cell."""
idcella = self._cell_id(r, c)
ubicazione = self._cell_label(r, c)
barcode_cella = self._cell_barcode(r, c)
if idcella <= 0:
messagebox.showwarning(
"Codice cella",
f"Codice cella non disponibile per {ubicazione}.",
parent=self,
)
return
messagebox.showinfo(
"Codice cella",
f"Ubicazione: {ubicazione}\nCodice cella: {idcella}\nBarcode cella: {barcode_cella or idcella}",
parent=self,
)
@_log_call()
def _prompt_carico(self, r: int, c: int):
"""Prompt the operator for a pallet barcode and move it to the selected cell."""
@@ -681,27 +701,7 @@ class LayoutWindow(ctk.CTkToplevel):
if stato <= 0:
self._toast("La cella selezionata non contiene alcuna UDC da scaricare.")
return
if stato >= 2:
self._open_scarico_dialog(r, c)
return
barcode = str(self.udc1[r][c] or "").strip()
if not barcode:
self._toast("UDC non disponibile per lo scarico.")
return
if not messagebox.askyesno(
"Scarico",
f"Scaricare l'UDC {barcode} da {self._cell_label(r, c)}?",
parent=self,
):
return
self._run_pallet_move(
barcode_pallet=barcode,
target_idcella=9999,
target_barcode_cella="9000000",
success_message=f"Scarico completato per {barcode}.",
busy_message=f"Scarico {barcode}...",
)
self._open_scarico_dialog(r, c)
@_log_call()
def _run_pallet_move(

View File

@@ -328,6 +328,8 @@ DET_COLS: List[ColSpec] = [
]
ROW_H = 28
RESERVED_IDSTATO_BG = "#dc2626"
RESERVED_IDSTATO_FG = "#ffffff"
# -------------------- Micro spinner (toolbar) --------------------
@@ -559,6 +561,8 @@ class ScrollTable(ctk.CTkFrame):
row_index: int,
anchors: Optional[List[str]] = None,
checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None,
cell_bg: Optional[Dict[str, str]] = None,
cell_fg: Optional[Dict[str, str]] = None,
):
"""Append one row to the table body."""
row_bg = TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD
@@ -568,8 +572,9 @@ class ScrollTable(ctk.CTkFrame):
row.pack_propagate(False)
for i, col in enumerate(self.columns):
holder_bg = (cell_bg or {}).get(col.key, row_bg)
holder = ctk.CTkFrame(
row, fg_color=row_bg,
row, fg_color=holder_bg,
width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR
)
@@ -581,10 +586,17 @@ class ScrollTable(ctk.CTkFrame):
cb = checkbox_builder(holder)
cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w")
else:
ctk.CTkLabel(holder, text="", fg_color=row_bg).pack(fill="both")
ctk.CTkLabel(holder, text="", fg_color=holder_bg).pack(fill="both")
else:
anchor = (anchors[i] if anchors else col.anchor)
ctk.CTkLabel(holder, text=values[i], anchor=anchor, fg_color=row_bg, text_color="#111827").pack(
ctk.CTkLabel(
holder,
text=values[i],
anchor=anchor,
fg_color=holder_bg,
text_color=(cell_fg or {}).get(col.key, "#111827"),
font=("Segoe UI", 10, "bold") if col.key == "IDStato" and col.key in (cell_bg or {}) else None,
).pack(
fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY
)
@@ -876,10 +888,22 @@ class GestionePickingListFrame(ctk.CTkFrame):
try:
for idx, d in enumerate(rows):
row_widget = self.pl_table.b_inner.winfo_children()[idx]
if int(d.get("IDStato") or 0) == 1:
row_widget.configure(fg_color="#ffe6f2") # rosa tenue
else:
row_widget.configure(fg_color="transparent")
row_bg = TABLE_ROW_EVEN if idx % 2 == 0 else TABLE_ROW_ODD
row_widget.configure(fg_color=row_bg)
row_children = row_widget.winfo_children()
if len(row_children) >= 5:
holder = row_children[4]
is_reserved = int(d.get("IDStato") or 0) == 1
holder_bg = RESERVED_IDSTATO_BG if is_reserved else row_bg
holder.configure(fg_color=holder_bg)
if holder.winfo_children():
lbl = holder.winfo_children()[0]
if hasattr(lbl, "configure"):
lbl.configure(
fg_color=holder_bg,
text_color=RESERVED_IDSTATO_FG if is_reserved else "#111827",
font=("Segoe UI", 10, "bold") if is_reserved else ("Segoe UI", 10),
)
except Exception:
pass
@@ -906,7 +930,9 @@ class GestionePickingListFrame(ctk.CTkFrame):
values=values,
row_index=r,
anchors=[c.anchor for c in PL_COLS],
checkbox_builder=model.build_checkbox
checkbox_builder=model.build_checkbox,
cell_bg={"IDStato": RESERVED_IDSTATO_BG} if int(d.get("IDStato") or 0) == 1 else None,
cell_fg={"IDStato": RESERVED_IDSTATO_FG} if int(d.get("IDStato") or 0) == 1 else None,
)
# 🎯 Colora dopo che la UI è resa → no balzi visivi
@@ -932,10 +958,20 @@ class GestionePickingListFrame(ctk.CTkFrame):
row_children = row_widget.winfo_children()
if len(row_children) >= 5:
holder = row_children[4]
holder.configure(fg_color=RESERVED_IDSTATO_BG if idstato == 1 else (
TABLE_ROW_EVEN if idx % 2 == 0 else TABLE_ROW_ODD
))
if holder.winfo_children():
lbl = holder.winfo_children()[0]
if hasattr(lbl, "configure"):
lbl.configure(text=str(idstato))
lbl.configure(
text=str(idstato),
fg_color=RESERVED_IDSTATO_BG if idstato == 1 else (
TABLE_ROW_EVEN if idx % 2 == 0 else TABLE_ROW_ODD
),
text_color=RESERVED_IDSTATO_FG if idstato == 1 else "#111827",
font=("Segoe UI", 10, "bold") if idstato == 1 else ("Segoe UI", 10),
)
except Exception:
pass
# differisci la colorazione (smooth)

View File

@@ -76,6 +76,12 @@ _MODULE_LOG_LEVEL = "DEBUG" if SCARICO_LOG_MODE.upper() == "DEBUG" else "INFO"
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
_MODULE_LOGGING_CONFIGURED = False
DEFAULT_SCARICO_USER = "warehouse_ui"
SHIPPED_IDCELLA = 9999
SHIPPED_BARCODE = "9000000"
SHIPPED_LABEL = "7G.1.1"
NON_SCAFF_IDCELLA = 1000
NON_SCAFF_BARCODE = "9001000"
NON_SCAFF_LABEL = "5E1.1"
def _session_login(session: UserSession | None, fallback: str | None = None) -> str:
@@ -198,28 +204,7 @@ last_move AS (
FROM dbo.MagazziniPallet mp
JOIN last_in_cell lic ON lic.LastID = mp.ID
),
latest_any AS (
SELECT
ranked.BarcodePallet,
ranked.IDCella
FROM (
SELECT
mp.Attributo AS BarcodePallet,
mp.IDCella,
ROW_NUMBER() OVER (
PARTITION BY mp.Attributo
ORDER BY mp.ID DESC
) AS rn
FROM dbo.MagazziniPallet mp
JOIN cell_pallets cp
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
mp.Attributo COLLATE Latin1_General_CI_AS
WHERE mp.Tipo = 'V'
AND mp.PesoUnitario > 0
) ranked
WHERE ranked.rn = 1
),
shipped AS (
closed_plist AS (
SELECT DISTINCT
shipped.BarcodePallet
FROM dbo.XMag_GiacenzaPalletPlistChiuse shipped
@@ -231,25 +216,19 @@ SELECT
cp.BarcodePallet AS UDC,
lm.ID AS SourceID,
lm.DataMagazzino AS LastEventAt,
:idcella AS CurrentIDCella,
CASE
WHEN shipped.BarcodePallet IS NOT NULL THEN CAST(1 AS int)
ELSE CAST(0 AS int)
END AS IsShippedGhost,
CASE
WHEN la.IDCella IS NOT NULL
AND la.IDCella <> :idcella
WHEN :idcella <> 9999
AND closed_plist.BarcodePallet IS NOT NULL
THEN CAST(1 AS int)
ELSE CAST(0 AS int)
END AS IsMovedGhost
END AS IsMissingUnload
FROM cell_pallets cp
LEFT JOIN last_move lm
ON lm.BarcodePallet COLLATE Latin1_General_CI_AS =
cp.BarcodePallet COLLATE Latin1_General_CI_AS
LEFT JOIN latest_any la
ON la.BarcodePallet COLLATE Latin1_General_CI_AS =
cp.BarcodePallet COLLATE Latin1_General_CI_AS
LEFT JOIN shipped
ON shipped.BarcodePallet COLLATE Latin1_General_CI_AS =
LEFT JOIN closed_plist
ON closed_plist.BarcodePallet COLLATE Latin1_General_CI_AS =
cp.BarcodePallet COLLATE Latin1_General_CI_AS
ORDER BY
lm.ID DESC,
@@ -413,19 +392,23 @@ class ScaricoRow:
udc: str
source_id: int | None
last_event_at: str
current_idcella: int
diagnostic_note: str
selected: tk.BooleanVar
def _build_diagnostic_note(is_shipped: int | bool, is_moved: int | bool) -> str:
def _build_diagnostic_note(is_missing_unload: int | bool = False) -> str:
"""Translate low-level anomaly flags into one operator-facing note."""
notes: list[str] = []
if bool(is_shipped):
notes.append("Mancato scarico: spedita")
if bool(is_moved):
notes.append("Mancato scarico: spostata")
return " | ".join(notes)
if bool(is_missing_unload):
return "Motivo: spedita ma non scaricata dalla cella"
return ""
def _is_missing_unload_row(row: ScaricoRow) -> bool:
"""Return True when the row must be corrected only with shipped unload."""
return "spedita ma non scaricata dalla cella" in str(row.diagnostic_note or "").lower()
class ScaricoDialog(ctk.CTkToplevel):
@@ -478,8 +461,9 @@ class ScaricoDialog(ctk.CTkToplevel):
self.wait_visibility()
except Exception:
pass
self.lift()
self.focus_force()
if self.winfo_exists():
self.lift()
self.focus_force()
def _build_ui(self):
"""Build the compact modal layout."""
@@ -546,19 +530,27 @@ class ScaricoDialog(ctk.CTkToplevel):
actions.grid_columnconfigure(0, weight=1)
ctk.CTkButton(
actions,
text=loc_text("scarico.button.submit", catalog=self._locale_catalog, default="Scarica"),
command=self._on_scarica,
text=loc_text("scarico.button.shipped", catalog=self._locale_catalog, default="Scarico come spedita"),
command=self._on_scarica_spedita,
font=theme_font(self._theme, "button_font", ("Segoe UI", 10, "bold")),
).grid(
row=0, column=1, padx=(8, 0), pady=8
)
ctk.CTkButton(
actions,
text=loc_text("scarico.button.non_shelved", catalog=self._locale_catalog, default="Scarico come non scaff."),
command=self._on_scarica_non_scaff,
font=theme_font(self._theme, "button_font", ("Segoe UI", 10, "bold")),
).grid(
row=0, column=2, padx=(8, 0), pady=8
)
ctk.CTkButton(
actions,
text=loc_text("scarico.button.close", catalog=self._locale_catalog, default="Chiudi"),
command=self._close,
font=theme_font(self._theme, "button_font", ("Segoe UI", 10, "bold")),
).grid(
row=0, column=2, padx=(8, 8), pady=8
row=0, column=3, padx=(8, 8), pady=8
)
def _render_rows(self):
@@ -617,7 +609,13 @@ class ScaricoDialog(ctk.CTkToplevel):
rows = res.get("rows", []) if isinstance(res, dict) else []
_log_dataset("scarico_load_rows", rows)
self.rows = []
for udc, source_id, last_event_at, is_shipped, is_moved in rows:
for (
udc,
source_id,
last_event_at,
current_idcella,
is_missing_unload,
) in rows:
if isinstance(last_event_at, datetime):
last_event = last_event_at.strftime("%d/%m/%Y %H:%M:%S")
else:
@@ -627,7 +625,8 @@ class ScaricoDialog(ctk.CTkToplevel):
udc=str(udc or ""),
source_id=int(source_id) if source_id is not None else None,
last_event_at=last_event,
diagnostic_note=_build_diagnostic_note(is_shipped, is_moved),
current_idcella=int(current_idcella or 0),
diagnostic_note=_build_diagnostic_note(is_missing_unload),
selected=tk.BooleanVar(value=False),
)
)
@@ -653,8 +652,9 @@ class ScaricoDialog(ctk.CTkToplevel):
)
@_log_call()
def _on_scarica(self):
"""Unload the UDCs selected by the user from the current cell."""
def _selected_rows_or_warn(self) -> list[ScaricoRow] | None:
"""Return selected rows or show the standard selection warning."""
selected = [row for row in self.rows if row.selected.get()]
if not selected:
messagebox.showinfo(
@@ -662,23 +662,97 @@ class ScaricoDialog(ctk.CTkToplevel):
loc_text("scarico.msg.select_one", catalog=self._locale_catalog, default="Seleziona almeno una UDC da scaricare."),
parent=self,
)
return selected
@_log_call()
def _on_scarica_spedita(self):
"""Unload selected UDCs to the shipped conventional location."""
self._scarica_selected_to_target(
target_idcella=SHIPPED_IDCELLA,
target_barcode_cella=SHIPPED_BARCODE,
target_label=SHIPPED_LABEL,
title="Scarico come spedita",
confirm_text="Scaricare come spedite {count} UDC da {ubicazione}?",
busy_message="Scarico UDC come spedite...",
block_shipped_for_non_scaff=False,
)
@_log_call()
def _on_scarica_non_scaff(self):
"""Unload selected UDCs to the non-shelved conventional location."""
self._scarica_selected_to_target(
target_idcella=NON_SCAFF_IDCELLA,
target_barcode_cella=NON_SCAFF_BARCODE,
target_label=NON_SCAFF_LABEL,
title="Scarico come non scaff.",
confirm_text="Scaricare come non scaffalate {count} UDC da {ubicazione}?",
busy_message="Scarico UDC come non scaffalate...",
block_shipped_for_non_scaff=True,
)
def _scarica_selected_to_target(
self,
*,
target_idcella: int,
target_barcode_cella: str,
target_label: str,
title: str,
confirm_text: str,
busy_message: str,
block_shipped_for_non_scaff: bool,
):
"""Move selected UDCs to one conventional unload location."""
selected = self._selected_rows_or_warn()
if not selected:
return
shipped_rows: list[ScaricoRow] = []
missing_unload_rows: list[ScaricoRow] = []
movable_rows = list(selected)
if block_shipped_for_non_scaff:
shipped_rows = [
row for row in selected
if int(row.current_idcella or 0) == SHIPPED_IDCELLA
]
missing_unload_rows = [row for row in selected if _is_missing_unload_row(row)]
blocked_udcs = {row.udc for row in shipped_rows + missing_unload_rows}
movable_rows = [row for row in selected if row.udc not in blocked_udcs]
if (shipped_rows or missing_unload_rows) and not movable_rows:
message_lines = ["Nessuna UDC movimentata."]
if shipped_rows:
message_lines.append(f"UDC gia' in {SHIPPED_LABEL}: " + ", ".join(row.udc for row in shipped_rows))
if missing_unload_rows:
message_lines.append(
"UDC spedite ma non scaricate: "
+ ", ".join(row.udc for row in missing_unload_rows)
)
message_lines.append("Usare 'Scarico come spedita'.")
messagebox.showwarning(
title,
"\n".join(message_lines),
parent=self,
)
return
if not messagebox.askyesno(
"Conferma scarico",
f"Scaricare {len(selected)} UDC da {self.ubicazione}?",
title,
confirm_text.format(count=len(movable_rows), ubicazione=self.ubicazione),
parent=self,
):
return
async def _job():
results: list[dict[str, Any]] = []
for row in selected:
for row in movable_rows:
result = await move_pallet_async(
self.db_client,
barcode_pallet=row.udc,
target_idcella=9999,
target_barcode_cella="9000000",
target_idcella=target_idcella,
target_barcode_cella=target_barcode_cella,
utente=_session_login(self.session),
)
results.append({"udc": row.udc, "affected": int(result.get("ok") or 0)})
@@ -690,33 +764,65 @@ class ScaricoDialog(ctk.CTkToplevel):
skipped = [item["udc"] for item in results if int(item.get("affected") or 0) <= 0]
if not done:
messagebox.showwarning(
"Scarica",
title,
"Nessuna UDC e' stata scaricata. Verifica che le unita' siano ancora presenti in cella.",
parent=self,
)
return
if skipped:
shipped_note = (
f"\nGia' in {SHIPPED_LABEL} non toccate: " + ", ".join(row.udc for row in shipped_rows)
if shipped_rows
else ""
)
missing_unload_note = (
"\nSpedite ma non scaricate, usare 'Scarico come spedita': "
+ ", ".join(row.udc for row in missing_unload_rows)
if missing_unload_rows
else ""
)
target_note = (
f"\nUDC in {target_label}: " + ", ".join(done)
if done
else ""
)
messagebox.showwarning(
"Scarica",
title,
"Scarico parziale.\nCompletate: "
+ ", ".join(done)
+ "\nNon scaricate: "
+ ", ".join(skipped),
+ ", ".join(skipped)
+ shipped_note
+ missing_unload_note
+ target_note,
parent=self,
)
else:
messagebox.showinfo(
"Scarica",
"Scarico completato per:\n" + "\n".join(done),
parent=self,
)
msg = f"Scarico completato verso {target_label} per:\n" + "\n".join(done)
if shipped_rows:
msg += f"\n\nUDC gia' in {SHIPPED_LABEL} non toccate:\n" + "\n".join(row.udc for row in shipped_rows)
msg += f"\n\nUDC in {target_label}: " + ", ".join(done)
if missing_unload_rows:
msg += (
"\n\nUDC spedite ma non scaricate non toccate:\n"
+ "\n".join(row.udc for row in missing_unload_rows)
+ "\nUsare 'Scarico come spedita'."
)
messagebox.showinfo(title, msg, parent=self)
log_user_action(
self.session,
module=MODULE_LOG_NAME,
action="layout.scarico",
action="layout.scarico.spedita" if int(target_idcella) == SHIPPED_IDCELLA else "layout.scarico.non_scaff",
outcome="ok",
target=self.ubicazione,
details={"scaricate": done, "saltate": skipped},
details={
"scaricate": done,
"saltate": skipped,
"target_idcella": target_idcella,
"target_barcode_cella": target_barcode_cella,
"gia_spedite": [row.udc for row in shipped_rows],
"spedite_non_scaricate": [row.udc for row in missing_unload_rows],
},
)
if self.on_completed:
self.on_completed()
@@ -727,7 +833,7 @@ class ScaricoDialog(ctk.CTkToplevel):
log_user_action(
self.session,
module=MODULE_LOG_NAME,
action="layout.scarico",
action="layout.scarico.spedita" if int(target_idcella) == SHIPPED_IDCELLA else "layout.scarico.non_scaff",
outcome="error",
target=self.ubicazione,
details={"error": str(ex)},
@@ -743,7 +849,7 @@ class ScaricoDialog(ctk.CTkToplevel):
_ok,
_err,
busy=self._busy,
message="Scarico UDC...",
message=busy_message,
)
@_log_call()

View File

@@ -10,6 +10,7 @@
"launcher.history_udc": "Storico movimenti UDC",
"launcher.pickinglist": "Gestione Picking List",
"launcher.history_pickinglist": "Storico Picking List",
"launcher.diagnostics": "Diagnostica",
"launcher.arrange": "Ridisponi finestre",
"launcher.exit": "Esci",
"launcher.already_running_title": "Warehouse",
@@ -75,6 +76,27 @@
"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}",
"diagnostics.title": "Diagnostica",
"diagnostics.button.shipped_not_disassociated": "UDC spedite non disassociate",
"diagnostics.button.multiple_udc_by_cell": "Celle con multiple UDC",
"diagnostics.button.sam_udc_not_in_wms": "Ultime UDC SAM non in WMS",
"diagnostics.button.export": "Esporta XLSX",
"diagnostics.busy.shipped_not_disassociated": "Cerco UDC spedite ma ancora in cella...",
"diagnostics.busy.multiple_udc_by_cell": "Cerco celle con multiple UDC...",
"diagnostics.busy.sam_udc_not_in_wms": "Cerco le ultime UDC presenti in SAM ma non ancora in WMS...",
"diagnostics.status.rows": "Righe trovate: {count}",
"diagnostics.col.udc": "UDC",
"diagnostics.col.cell": "CELLA",
"diagnostics.col.lot": "Lotto",
"diagnostics.col.product": "Prodotto",
"diagnostics.col.description": "Descrizione",
"diagnostics.col.shipped_not_disassociated": "Spedita ma non disassociata",
"diagnostics.col.symbolic_cell": "Nome simbolico cella",
"diagnostics.export.title": "Esporta",
"diagnostics.export.empty": "Non ci sono righe da esportare.",
"diagnostics.export.dep": "Per l'esportazione serve 'openpyxl' (pip install openpyxl).",
"diagnostics.export.ok": "File creato:\n{path}",
"diagnostics.export.error": "Errore durante l'esportazione:\n{error}",
"reset.title": "Gestione Corsie - svuotamento celle per corsia",
"reset.label.aisle": "Corsia:",
"reset.button.refresh": "Carica",
@@ -107,6 +129,8 @@
"scarico.col.last_insert": "Ultimo inserimento",
"scarico.col.diagnostic": "Diagnostica",
"scarico.button.submit": "Scarica",
"scarico.button.shipped": "Scarico come spedita",
"scarico.button.non_shelved": "Scarico come non scaff.",
"scarico.button.close": "Chiudi",
"scarico.msg.title": "Scarica",
"scarico.msg.select_one": "Seleziona almeno una UDC da scaricare.",
@@ -123,6 +147,7 @@
"launcher.history_udc": "UDC Movement History",
"launcher.pickinglist": "Picking List Management",
"launcher.history_pickinglist": "Picking List History",
"launcher.diagnostics": "Diagnostics",
"launcher.arrange": "Arrange windows",
"launcher.exit": "Exit",
"launcher.already_running_title": "Warehouse",
@@ -188,6 +213,27 @@
"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}",
"diagnostics.title": "Diagnostics",
"diagnostics.button.shipped_not_disassociated": "Shipped UDC not disassociated",
"diagnostics.button.multiple_udc_by_cell": "Cells with multiple UDC",
"diagnostics.button.sam_udc_not_in_wms": "Latest UDC in SAM not in WMS",
"diagnostics.button.export": "Export XLSX",
"diagnostics.busy.shipped_not_disassociated": "Searching shipped UDC still assigned to cells...",
"diagnostics.busy.multiple_udc_by_cell": "Searching cells with multiple UDC...",
"diagnostics.busy.sam_udc_not_in_wms": "Searching latest UDC present in SAM but not yet in WMS...",
"diagnostics.status.rows": "Rows found: {count}",
"diagnostics.col.udc": "UDC",
"diagnostics.col.cell": "CELL",
"diagnostics.col.lot": "Lot",
"diagnostics.col.product": "Product",
"diagnostics.col.description": "Description",
"diagnostics.col.shipped_not_disassociated": "Shipped but not disassociated",
"diagnostics.col.symbolic_cell": "Symbolic cell name",
"diagnostics.export.title": "Export",
"diagnostics.export.empty": "There are no rows to export.",
"diagnostics.export.dep": "Export requires 'openpyxl' (pip install openpyxl).",
"diagnostics.export.ok": "File created:\n{path}",
"diagnostics.export.error": "Export failed:\n{error}",
"reset.title": "Aisle Management - empty cells by aisle",
"reset.label.aisle": "Aisle:",
"reset.button.refresh": "Load",
@@ -220,6 +266,8 @@
"scarico.col.last_insert": "Last insert",
"scarico.col.diagnostic": "Diagnostics",
"scarico.button.submit": "Unload",
"scarico.button.shipped": "Unload as shipped",
"scarico.button.non_shelved": "Unload as non-shelved",
"scarico.button.close": "Close",
"scarico.msg.title": "Unload",
"scarico.msg.select_one": "Select at least one UDC to unload.",

11
main.py
View File

@@ -22,6 +22,7 @@ from async_loop_singleton import get_global_loop, stop_global_loop
from async_msssql_query import AsyncMSSQLClient
from audit_log import log_session_event
from db_config import build_dsn_from_config, ensure_db_config
from diagnostica import open_diagnostica_window
from gestione_layout import open_layout_window
from gestione_pickinglist import open_pickinglist_window
from login_window import prompt_login
@@ -161,6 +162,7 @@ class Launcher(ctk.CTk):
"storico_udc",
"pickinglist",
"storico_pickinglist",
"diagnostica",
]
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
@@ -268,6 +270,15 @@ class Launcher(ctk.CTk):
lambda: open_storico_pickinglist_window(self, self.db_client, session=self.session),
),
),
(
"diagnostica",
loc_text("launcher.diagnostics", catalog=self._locale_catalog, default="Diagnostica"),
"launcher.open_diagnostics",
lambda: self._open_or_focus_child_window(
"diagnostica",
lambda: open_diagnostica_window(self, self.db_client, session=self.session),
),
),
(
"arrange",
loc_text("launcher.arrange", catalog=self._locale_catalog, default="Ridisponi finestre"),

View File

@@ -303,6 +303,23 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, A
rc = int(rows[0].get("RC") or 0)
except Exception:
rc = 0
if rc == 0 and azione == "P":
resolve_sql = """
UPDATE dbo.py_BarcodePickingListSkip
SET Risolto = 1
WHERE Documento COLLATE Latin1_General_CI_AS =
CAST(:Documento AS varchar(50)) COLLATE Latin1_General_CI_AS
AND Risolto = 0;
"""
_log_sql("resolve_skipped_pallets_on_prenota", resolve_sql, {"Documento": Documento})
if hasattr(db, "exec"):
await db.exec(resolve_sql, {"Documento": Documento}, commit=True)
else:
await db.query_json(
resolve_sql + "\nSELECT 0 AS RC;",
{"Documento": Documento},
commit=True,
)
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Stored procedure completata documento={Documento} azione={azione} rc={rc}")
return SPResult(rc=rc, message="", id_result=None)
except Exception as exc:

View File

@@ -0,0 +1,9 @@
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID(N'dbo.py_BarcodePickingListSkip', N'U') IS NOT NULL
BEGIN
DROP TABLE dbo.py_BarcodePickingListSkip;
END;
COMMIT TRANSACTION;

View File

@@ -93,7 +93,7 @@ SELECT
FROM meta m
LEFT JOIN agg a ON a.Documento = m.Documento
WHERE (:documento IS NULL OR CAST(m.Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
ORDER BY m.Documento DESC;
ORDER BY m.DataDocumento DESC, m.Documento DESC;
"""
# Detail query:

View File

@@ -8,6 +8,7 @@
"launcher.open_history_udc": "Apre lo storico movimenti UDC per ricostruire carichi, scarichi, celle e utenti coinvolti.",
"launcher.open_pickinglist": "Apre la gestione delle picking list per prenotare, controllare e aggiornare le liste di prelievo.",
"launcher.open_history_pickinglist": "Apre lo storico picking list per consultare liste, stato operativo e dettaglio UDC.",
"launcher.open_diagnostics": "Apre la diagnostica operativa con quadrature e controlli di coerenza sui dati di magazzino.",
"launcher.arrange_windows": "Dispone in cascata le finestre aperte seguendo l'ordine dei pulsanti del launcher.",
"launcher.exit": "Chiude l'applicazione in modo pulito terminando la sessione utente e rilasciando la connessione condivisa al database.",
"dbconfig.heading": "Spiega che qui si inseriscono i dati minimi per permettere al programma di collegarsi al database del magazzino al primo avvio.",
@@ -42,6 +43,7 @@
"launcher.open_history_udc": "Open UDC movement history to review loads, unloads, cells and users involved.",
"launcher.open_pickinglist": "Open picking list management to reserve, inspect and update picking lists.",
"launcher.open_history_pickinglist": "Open picking-list history to inspect lists, operational status and UDC detail.",
"launcher.open_diagnostics": "Open operational diagnostics with data consistency checks and warehouse quadratures.",
"launcher.arrange_windows": "Arrange open windows in cascade order following the launcher buttons.",
"launcher.exit": "Close the application cleanly by ending the user session and releasing the shared database connection.",
"dbconfig.heading": "Explain that this form collects the minimum data needed to connect the program to the warehouse database on first startup.",

View File

@@ -20,6 +20,7 @@ ALL_ACTIONS: FrozenSet[str] = frozenset(
"launcher.open_history_udc",
"launcher.open_pickinglist",
"launcher.open_history_pickinglist",
"launcher.open_diagnostics",
"launcher.arrange_windows",
"launcher.exit",
"reset_corsie.view",
@@ -27,6 +28,7 @@ ALL_ACTIONS: FrozenSet[str] = frozenset(
"non_shelved.view",
"history_udc.view",
"history_pickinglist.view",
"diagnostics.view",
"multi_udc.view",
"layout.view",
"layout.carico",

View File

@@ -10,30 +10,31 @@ APP_VERSION = "1.0.0"
MODULE_VERSIONS: dict[str, str] = {
"app": APP_VERSION,
"async_loop_singleton": "1.0.0",
"async_msssql_query": "1.0.0",
"async_msssql_query": "1.0.1",
"audit_log": "1.0.0",
"main": "1.0.1",
"barcode_client": "1.0.17",
"barcode_repository": "1.0.4",
"barcode_service": "1.0.13",
"main": "1.0.2",
"barcode_client": "1.0.26",
"barcode_repository": "1.0.12",
"barcode_service": "1.0.24",
"busy_overlay": "1.0.0",
"db_config": "1.0.0",
"diagnostica": "1.0.4",
"gestione_aree": "1.0.1",
"gestione_layout": "1.0.0",
"gestione_pickinglist": "1.0.2",
"gestione_scarico": "1.0.0",
"gestione_layout": "1.0.2",
"gestione_pickinglist": "1.0.4",
"gestione_scarico": "1.0.8",
"locale_text": "1.0.0",
"login_window": "1.0.0",
"prenota_sprenota_sql": "1.0.0",
"prenota_sprenota_sql": "1.0.1",
"reset_corsie": "1.0.0",
"runtime_support": "1.0.1",
"search_pallets": "1.0.0",
"storico_pickinglist": "1.0.3",
"storico_pickinglist": "1.0.4",
"storico_udc": "1.0.0",
"tooltips": "1.0.0",
"udc_non_scaffalate": "1.0.1",
"ui_theme": "1.0.0",
"user_session": "1.0.0",
"user_session": "1.0.1",
"view_celle_multi_udc": "1.0.0",
"window_placement": "1.0.0",
}