diff --git a/apply_barcode_picking_skip_patch.sql b/apply_barcode_picking_skip_patch.sql index 5a155bd..bc83650 100644 --- a/apply_barcode_picking_skip_patch.sql +++ b/apply_barcode_picking_skip_patch.sql @@ -5,8 +5,8 @@ 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) NOT NULL, - Pallet varchar(50) NOT NULL, + 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(), diff --git a/apply_online_history_forms_patch.sql b/apply_online_history_forms_patch.sql index eb26471..414db07 100644 --- a/apply_online_history_forms_patch.sql +++ b/apply_online_history_forms_patch.sql @@ -72,7 +72,7 @@ LEFT OUTER JOIN SAMA1.sam.EXTUC LEFT OUTER JOIN SAMA1.dbo.MTRASP ON SAMA1.dbo.BAMTES.IDMTRASP = SAMA1.dbo.MTRASP.ID WHERE - SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE()) + SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE()) - 1 AND SAMA1.dbo.BAMTES.STATO IN ('P', 'D') AND SAMA1.dbo.LOTSER.NUMLOT <> '00000000000' GROUP BY diff --git a/apply_online_python_wms_full_patch.sql b/apply_online_python_wms_full_patch.sql new file mode 100644 index 0000000..7e501e9 --- /dev/null +++ b/apply_online_python_wms_full_patch.sql @@ -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()) - 1 +per mantenere visibili anche le picking list dell'anno precedente. +*/ + +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()) - 1 + 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 diff --git a/apply_python_pickinglist_history_views.sql b/apply_python_pickinglist_history_views.sql index f3ee304..3045067 100644 --- a/apply_python_pickinglist_history_views.sql +++ b/apply_python_pickinglist_history_views.sql @@ -48,7 +48,7 @@ LEFT OUTER JOIN SAMA1.sam.EXTUC LEFT OUTER JOIN SAMA1.dbo.MTRASP ON SAMA1.dbo.BAMTES.IDMTRASP = SAMA1.dbo.MTRASP.ID WHERE - SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE()) + SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE()) - 1 AND SAMA1.dbo.BAMTES.STATO IN ('P', 'D') AND SAMA1.dbo.LOTSER.NUMLOT <> '00000000000' GROUP BY diff --git a/barcode_client.py b/barcode_client.py index 18d0e66..27b1b0a 100644 --- a/barcode_client.py +++ b/barcode_client.py @@ -68,6 +68,9 @@ 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._pallet_user_input_serial = 0 + self._priority_loaded_input_serial = 0 + self._priority_expected_pallet = "" self._status_colors = { "red": "#f4cccc", "green": "#d9ead3", @@ -237,16 +240,20 @@ class BarcodeClientApp: parent.columnconfigure(1, weight=1) if scanned: self.pallet_entry = entry + self.pallet_entry.bind("", 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 +267,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 +280,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 +387,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,25 +398,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") - if state.mode in ("priority_high", "priority_low"): - self.btn_unload.configure(text="[F4] Salta UDC") - else: - self.btn_unload.configure(text="[F4] Scarica") - 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: @@ -419,6 +440,22 @@ class BarcodeClientApp: 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: self.pallet_entry.focus_force() @@ -448,6 +485,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() @@ -455,7 +500,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" @@ -510,7 +563,10 @@ class BarcodeClientApp: 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 diff --git a/barcode_repository.py b/barcode_repository.py index 96be675..f022a4d 100644 --- a/barcode_repository.py +++ b/barcode_repository.py @@ -32,8 +32,10 @@ WHERE Ordinamento > 0 AND NOT EXISTS ( SELECT 1 FROM dbo.py_BarcodePickingListSkip AS s - WHERE s.Documento = CAST(pl.Documento AS varchar(50)) - AND s.Pallet = CAST(pl.Pallet AS varchar(50)) + 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 ) @@ -52,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; """ @@ -63,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; """ @@ -77,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 = """ @@ -90,7 +92,7 @@ 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; """ @@ -103,6 +105,19 @@ WHERE g.IDCella = :id_cella ORDER BY g.BarcodePallet; """ +SQL_CLOSED_PICKING_BY_PALLET = """ +SELECT TOP (1) + BarcodePallet, + NumeroPallet, + IDMagazzino, + IDArea, + IDCella, + IDStato +FROM dbo.XMag_GiacenzaPalletPlistChiuse +WHERE BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS +ORDER BY IDCella DESC; +""" + SQL_RESOLVE_PHYSICAL_CELL = """ DECLARE @raw int = TRY_CONVERT(int, :destination); DECLARE @cell_id int = @@ -151,7 +166,7 @@ IF NOT EXISTS ( SELECT 1 FROM dbo.py_BarcodePickingListSkip WHERE Documento = :documento - AND Pallet = :pallet + AND Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS AND IDStato = :id_stato AND Risolto = 0 ) @@ -182,6 +197,37 @@ SELECT :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.""" @@ -265,6 +311,13 @@ class BarcodeRepository: 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, *, @@ -304,6 +357,35 @@ class BarcodeRepository: ), ) + 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.""" diff --git a/barcode_service.py b/barcode_service.py index 266ca84..73aa98d 100644 --- a/barcode_service.py +++ b/barcode_service.py @@ -52,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): @@ -160,6 +160,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", @@ -215,6 +233,11 @@ 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) @@ -351,7 +374,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}") @@ -377,6 +400,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 @@ -451,6 +478,30 @@ class BarcodeService: return await self.submit(scanned_pallet=pallet, destination_barcode=self.NON_SCAFFALATA_BARCODE) + 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, *, @@ -505,7 +556,7 @@ class BarcodeService: 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 5000 + auto_advance_delay_ms = 3000 if is_recovered_picking else 0 state = await self._build_post_move_state( barcode_pallet=pallet, destination_barcode=destination, @@ -542,7 +593,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( @@ -569,14 +628,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) @@ -607,6 +672,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.""" diff --git a/diagnostica_scarico_celle_multiple.md b/diagnostica_scarico_celle_multiple.md new file mode 100644 index 0000000..a31a308 --- /dev/null +++ b/diagnostica_scarico_celle_multiple.md @@ -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 = , + RisoltoDataOra = SYSDATETIME() + WHERE 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. diff --git a/gestione_layout.py b/gestione_layout.py index f5d3aa2..4e5bb7c 100644 --- a/gestione_layout.py +++ b/gestione_layout.py @@ -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.""" diff --git a/gestione_pickinglist.py b/gestione_pickinglist.py index 636b1f8..56cb6a7 100644 --- a/gestione_pickinglist.py +++ b/gestione_pickinglist.py @@ -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) diff --git a/gestione_scarico.py b/gestione_scarico.py index 247b89b..d747ccf 100644 --- a/gestione_scarico.py +++ b/gestione_scarico.py @@ -204,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 @@ -237,26 +216,19 @@ SELECT cp.BarcodePallet AS UDC, lm.ID AS SourceID, lm.DataMagazzino AS LastEventAt, - ISNULL(la.IDCella, :idcella) AS CurrentIDCella, + :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, @@ -425,15 +397,18 @@ class ScaricoRow: 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): @@ -486,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.""" @@ -633,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, current_idcella, 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: @@ -644,7 +626,7 @@ class ScaricoDialog(ctk.CTkToplevel): source_id=int(source_id) if source_id is not None else None, last_event_at=last_event, current_idcella=int(current_idcella or 0), - diagnostic_note=_build_diagnostic_note(is_shipped, is_moved), + diagnostic_note=_build_diagnostic_note(is_missing_unload), selected=tk.BooleanVar(value=False), ) ) @@ -728,21 +710,30 @@ class ScaricoDialog(ctk.CTkToplevel): 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 - or "spedita" in str(row.diagnostic_note or "").lower() ] - shipped_udcs = {row.udc for row in shipped_rows} - movable_rows = [row for row in selected if row.udc not in shipped_udcs] + 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 and not movable_rows: + 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, - "Nessuna UDC movimentata.\n" - f"UDC in {SHIPPED_LABEL}: " + ", ".join(row.udc for row in shipped_rows), + "\n".join(message_lines), parent=self, ) return @@ -784,6 +775,12 @@ class ScaricoDialog(ctk.CTkToplevel): 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 @@ -796,6 +793,7 @@ class ScaricoDialog(ctk.CTkToplevel): + "\nNon scaricate: " + ", ".join(skipped) + shipped_note + + missing_unload_note + target_note, parent=self, ) @@ -804,6 +802,12 @@ class ScaricoDialog(ctk.CTkToplevel): 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, @@ -817,6 +821,7 @@ class ScaricoDialog(ctk.CTkToplevel): "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: diff --git a/prenota_sprenota_sql.py b/prenota_sprenota_sql.py index 8e22b8c..3f57737 100644 --- a/prenota_sprenota_sql.py +++ b/prenota_sprenota_sql.py @@ -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: diff --git a/version_info.py b/version_info.py index f3f9a67..14ad195 100644 --- a/version_info.py +++ b/version_info.py @@ -13,18 +13,18 @@ MODULE_VERSIONS: dict[str, str] = { "async_msssql_query": "1.0.0", "audit_log": "1.0.0", "main": "1.0.1", - "barcode_client": "1.0.20", - "barcode_repository": "1.0.6", - "barcode_service": "1.0.16", + "barcode_client": "1.0.24", + "barcode_repository": "1.0.10", + "barcode_service": "1.0.22", "busy_overlay": "1.0.0", "db_config": "1.0.0", "gestione_aree": "1.0.1", - "gestione_layout": "1.0.1", - "gestione_pickinglist": "1.0.2", - "gestione_scarico": "1.0.1", + "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",