From bd844ce05614d2cb42ce0420a3c54e7c39f0c8a1 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Thu, 18 Jun 2026 18:19:00 +0200 Subject: [PATCH] Stabilizza barcode e UDC non scaffalate --- barcode_client.py | 40 ++++++++++++++++++-- gestione_aree.py | 47 +++++++++++++++++++---- udc_non_scaffalate.py | 87 ++++++++++++++++++++++++++++++++----------- version_info.py | 6 +-- 4 files changed, 145 insertions(+), 35 deletions(-) diff --git a/barcode_client.py b/barcode_client.py index 4bed4a0..075f220 100644 --- a/barcode_client.py +++ b/barcode_client.py @@ -48,6 +48,7 @@ def _build_bypass_session(): class BarcodeClientApp: """Single-window Tk barcode client modeled after the C# legacy form.""" + PALLET_BARCODE_LENGTH = 6 NON_SCAFFALATA_BARCODE = "9001000" SHIPPED_BARCODE = "9000000" BARCODE_MAX_WIDTH = 320 @@ -65,6 +66,7 @@ class BarcodeClientApp: self.service = BarcodeService(self.repository, session.operator_id) self._pending: Future | None = None self._auto_advance_id: str | None = None + self._pallet_auto_focus_id: str | None = None self._status_colors = { "red": "#f4cccc", "green": "#d9ead3", @@ -234,7 +236,7 @@ class BarcodeClientApp: parent.columnconfigure(1, weight=1) if scanned: self.pallet_entry = entry - self.scanned_var.trace_add("write", lambda *_: self._limit_var(self.scanned_var, 8)) + 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)) @@ -244,6 +246,32 @@ class BarcodeClientApp: if len(value) > max_len: variable.set(value[:max_len]) + def _on_scanned_var_changed(self) -> None: + self._limit_var(self.scanned_var, 8) + if self._pallet_auto_focus_id is not None: + try: + self.root.after_cancel(self._pallet_auto_focus_id) + except Exception: + pass + self._pallet_auto_focus_id = None + pallet = str(self.scanned_var.get() or "").strip() + if len(pallet) < self.PALLET_BARCODE_LENGTH: + return + self._pallet_auto_focus_id = self.root.after(80, self._auto_focus_destination_after_scan) + + def _auto_focus_destination_after_scan(self) -> None: + self._pallet_auto_focus_id = None + pallet = str(self.scanned_var.get() or "").strip() + if not pallet: + return + if self._pending is not None and not self._pending.done(): + return + if getattr(self.service.state, "mode", "") == "confirm": + return + if bool(getattr(self.service.state, "destination_readonly", False)): + return + self._focus_destination_input() + def _apply_responsive_geometry(self) -> None: """Adapt the window size to barcode-sized or desktop-sized screens.""" @@ -331,6 +359,12 @@ class BarcodeClientApp: except Exception: pass self._auto_advance_id = None + if self._pallet_auto_focus_id is not None: + try: + self.root.after_cancel(self._pallet_auto_focus_id) + except Exception: + pass + self._pallet_auto_focus_id = None self.queue_var.set(state.queue_label) self.destination_var.set(state.destination_barcode) self.scanned_var.set(state.scanned_pallet) @@ -425,7 +459,7 @@ class BarcodeClientApp: def _start_queue(self, id_stato: int) -> None: self._run_async( lambda: self.service.start_priority_queue(id_stato), - busy_message="Carico la coda selezionata...", + busy_message="In preparazione...", ) def _submit(self) -> None: @@ -434,7 +468,7 @@ class BarcodeClientApp: scanned_pallet=self.scanned_var.get(), destination_barcode=self.destination_var.get(), ), - busy_message="Eseguo il movimento...", + busy_message="In esecuzione...", ) def _run_async(self, coro_factory: Callable[[], object], busy_message: str) -> None: diff --git a/gestione_aree.py b/gestione_aree.py index 18f3589..19737c2 100644 --- a/gestione_aree.py +++ b/gestione_aree.py @@ -19,6 +19,7 @@ from typing import Any, Callable, Optional import customtkinter as ctk from async_loop_singleton import get_global_loop +from runtime_support import log_exception from version_info import module_version __version__ = module_version(__name__) @@ -128,21 +129,53 @@ class AsyncRunner: busy.show(message) fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop) + def _widget_alive() -> bool: + try: + return bool(self.widget.winfo_exists()) + except tk.TclError: + return False + + def _dispatch_success(res: Any) -> None: + try: + on_success(res) + except BaseException as ex: + log_exception(__name__, ex, context="AsyncRunner on_success") + + def _dispatch_error(ex: BaseException) -> None: + if on_error: + try: + on_error(ex) + except BaseException as callback_ex: + log_exception(__name__, callback_ex, context="AsyncRunner on_error") + else: + log_exception(__name__, ex, context="AsyncRunner unhandled error") + def _poll(): + if not _widget_alive(): + return if fut.done(): if busy: - busy.hide() + try: + busy.hide() + except Exception as ex: + log_exception(__name__, ex, context="AsyncRunner hide busy") try: res = fut.result() except BaseException as ex: - if on_error: - self.widget.after(0, lambda e=ex: on_error(e)) - else: - print("[AsyncRunner] Unhandled error:", repr(ex)) + try: + self.widget.after(0, lambda e=ex: _dispatch_error(e)) + except tk.TclError: + log_exception(__name__, ex, context="AsyncRunner error after destroyed") else: - self.widget.after(0, lambda r=res: on_success(r)) + try: + self.widget.after(0, lambda r=res: _dispatch_success(r)) + except tk.TclError as ex: + log_exception(__name__, ex, context="AsyncRunner success after destroyed") else: - self.widget.after(60, _poll) + try: + self.widget.after(60, _poll) + except tk.TclError: + return _poll() diff --git a/udc_non_scaffalate.py b/udc_non_scaffalate.py index b3b770e..6bf3fd7 100644 --- a/udc_non_scaffalate.py +++ b/udc_non_scaffalate.py @@ -11,6 +11,7 @@ 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 @@ -20,7 +21,7 @@ __version__ = module_version(__name__) SQL_NON_SCAFFALATE = """ -WITH trace AS ( +WITH trace_rows AS ( SELECT Pallet, MIN(Lotto) AS Lotto, @@ -39,7 +40,7 @@ SELECT FROM dbo.XMag_GiacenzaPallet AS g LEFT JOIN dbo.Celle AS c ON c.ID = g.IDCella -LEFT JOIN trace AS t +LEFT JOIN trace_rows AS t ON t.Pallet COLLATE Latin1_General_CI_AS = g.BarcodePallet COLLATE Latin1_General_CI_AS WHERE g.IDCella = 1000 @@ -72,6 +73,7 @@ class UDCNonScaffalateWindow(ctk.CTkToplevel): self._locale_catalog = load_locale_catalog() self._async = AsyncRunner(self) self._busy = InlineBusyOverlay(self, self._theme) + self._load_in_progress = False self.title(versioned_title(loc_text("non_shelved.title", catalog=self._locale_catalog, default="UDC non scaffalate"), __name__)) self.geometry(str(theme_value(self._theme, "window_geometry", "1000x650"))) @@ -85,6 +87,18 @@ class UDCNonScaffalateWindow(ctk.CTkToplevel): self._build_ui() self.after(250, self._load) + 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 UDC non scaffalate") + def _build_ui(self) -> None: self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) @@ -145,33 +159,62 @@ class UDCNonScaffalateWindow(ctk.CTkToplevel): sx.grid(row=1, column=0, sticky="ew") def _load(self) -> None: + if self._load_in_progress or not self._is_alive(): + return + async def job(): return await self.db_client.query_json(SQL_NON_SCAFFALATE, as_dict_rows=True) - self._busy.show(loc_text("non_shelved.busy", catalog=self._locale_catalog, default="Carico UDC non scaffalate...")) - self._async.run(job(), self._on_loaded, self._on_error) + self._load_in_progress = True + try: + self._busy.show(loc_text("non_shelved.busy", catalog=self._locale_catalog, default="Carico UDC non scaffalate...")) + 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="avvio caricamento UDC non scaffalate") + messagebox.showerror( + loc_text("non_shelved.title", catalog=self._locale_catalog, default="UDC non scaffalate"), + str(exc), + parent=self if self._is_alive() else None, + ) def _on_loaded(self, res: dict[str, Any] | None) -> None: - self._busy.hide() - rows = _rows_to_dicts(res) - self.tree.delete(*self.tree.get_children("")) - for index, row in enumerate(rows): - self.tree.insert( - "", - "end", - values=( - row.get("UDC") or "", - row.get("IDCella") or "", - row.get("Ubicazione") or "", - row.get("Lotto") or "", - row.get("Prodotto") or "", - row.get("Descrizione") or "", - ), - tags=(zebra_tag(index),), + 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("")) + for index, row in enumerate(rows): + self.tree.insert( + "", + "end", + values=( + row.get("UDC") or "", + row.get("IDCella") or "", + row.get("Ubicazione") or "", + row.get("Lotto") or "", + row.get("Prodotto") or "", + row.get("Descrizione") or "", + ), + tags=(zebra_tag(index),), + ) + except Exception as exc: + log_exception(__name__, exc, context="render UDC non scaffalate") + messagebox.showerror( + loc_text("non_shelved.title", catalog=self._locale_catalog, default="UDC non scaffalate"), + str(exc), + parent=self, ) def _on_error(self, exc: BaseException) -> None: - self._busy.hide() + self._load_in_progress = False + self._hide_busy_safe() + log_exception(__name__, exc, context="query UDC non scaffalate") + if not self._is_alive(): + return messagebox.showerror( loc_text("non_shelved.title", catalog=self._locale_catalog, default="UDC non scaffalate"), str(exc), @@ -183,5 +226,5 @@ def open_udc_non_scaffalate_window(parent: tk.Misc, db_client, session=None) -> """Open the non-shelved UDC window.""" win = UDCNonScaffalateWindow(parent, db_client, session=session) - place_window_fullsize_below_parent_later(win, parent) + place_window_fullsize_below_parent_later(parent, win) return win diff --git a/version_info.py b/version_info.py index 03ac364..453f8b9 100644 --- a/version_info.py +++ b/version_info.py @@ -13,12 +13,12 @@ MODULE_VERSIONS: dict[str, str] = { "async_msssql_query": "1.0.0", "audit_log": "1.0.0", "main": "1.0.1", - "barcode_client": "1.0.10", + "barcode_client": "1.0.12", "barcode_repository": "1.0.3", "barcode_service": "1.0.7", "busy_overlay": "1.0.0", "db_config": "1.0.0", - "gestione_aree": "1.0.0", + "gestione_aree": "1.0.1", "gestione_layout": "1.0.0", "gestione_pickinglist": "1.0.2", "gestione_scarico": "1.0.0", @@ -31,7 +31,7 @@ MODULE_VERSIONS: dict[str, str] = { "storico_pickinglist": "1.0.3", "storico_udc": "1.0.0", "tooltips": "1.0.0", - "udc_non_scaffalate": "1.0.0", + "udc_non_scaffalate": "1.0.1", "ui_theme": "1.0.0", "user_session": "1.0.0", "view_celle_multi_udc": "1.0.0",