"""Application entry point for the warehouse desktop tool. This module wires together the shared async database client, the global background event loop and the different Tk/CustomTkinter windows exposed by the project. """ import asyncio import ctypes import sys import tkinter as tk import customtkinter as ctk from tkinter import messagebox from async_loop_singleton import get_global_loop from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn from gestione_layout import open_layout_window from gestione_pickinglist import open_pickinglist_window from login_window import prompt_login from reset_corsie import open_reset_corsie_window from search_pallets import open_search_window from audit_log import log_session_event from view_celle_multi_udc import open_celle_multiple_window from user_session import UserSession, create_user_session # ---- Config ---- SERVER = r"mde3\gesterp" DBNAME = "Mediseawall" USER = "sa" PASSWORD = "1Password1" # Development shortcut: skip the login dialog and boot directly as MAG1. # Set to False when you want to restore normal authentication. BYPASS_LOGIN = True BYPASS_LOGIN_USER = { "operator_id": 4, "login": "MAG1", "nominativo": "MAG1", "privilegio": 3, "codice_unita": "U1", } if sys.platform.startswith("win"): try: asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) except Exception: pass # Create one global loop and make it the default everywhere. _loop = get_global_loop() asyncio.set_event_loop(_loop) def _noop(*args, **kwargs): """Compatibility no-op used when optional Tk DPI hooks are missing.""" return None if not hasattr(tk.Toplevel, "block_update_dimensions_event"): tk.Toplevel.block_update_dimensions_event = _noop # type: ignore[attr-defined] if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"): tk.Toplevel.unblock_update_dimensions_event = _noop # type: ignore[attr-defined] dsn_app = make_mssql_dsn(server=SERVER, database=DBNAME, user=USER, password=PASSWORD) db_app = AsyncMSSQLClient(dsn_app) _APP_MUTEX = None _APP_MUTEX_NAME = "Local\\WarehousePythonAppSingleton" def _acquire_single_instance_mutex() -> bool: """Return ``True`` only for the first running instance of the application.""" global _APP_MUTEX if not sys.platform.startswith("win"): return True kernel32 = ctypes.windll.kernel32 mutex = kernel32.CreateMutexW(None, False, _APP_MUTEX_NAME) if not mutex: return True last_error = kernel32.GetLastError() _APP_MUTEX = mutex ERROR_ALREADY_EXISTS = 183 return last_error != ERROR_ALREADY_EXISTS def _build_bypass_session() -> UserSession: """Create the development session used when authentication is bypassed.""" return create_user_session( operator_id=int(BYPASS_LOGIN_USER["operator_id"]), login=str(BYPASS_LOGIN_USER["login"]), nominativo=str(BYPASS_LOGIN_USER["nominativo"]), privilegio=int(BYPASS_LOGIN_USER["privilegio"]), codice_unita=str(BYPASS_LOGIN_USER["codice_unita"]), ) class Launcher(ctk.CTk): """Main launcher window that exposes the available warehouse tools.""" def __init__(self, session: UserSession): """Create the launcher toolbar and wire every button to a feature window.""" super().__init__() self.session: UserSession = session self.title(f"Warehouse 1.0.0 - {self.session.display_name}") self.geometry("1280x96+0+0") wrap = ctk.CTkFrame(self) wrap.pack(pady=10, fill="x") info = ctk.CTkLabel( wrap, text=f"Operatore: {self.session.display_name} ({self.session.login})", anchor="w", font=("", 12, "bold"), ) info.grid(row=0, column=0, columnspan=5, padx=6, pady=(4, 2), sticky="ew") ctk.CTkButton( wrap, text="Gestione Corsie", state="normal" if self.session.can("launcher.open_reset_corsie") else "disabled", command=lambda: open_reset_corsie_window(self, db_app, session=self.session), ).grid(row=1, column=0, padx=6, pady=6, sticky="ew") ctk.CTkButton( wrap, text="Gestione Layout", state="normal" if self.session.can("launcher.open_layout") else "disabled", command=lambda: open_layout_window(self, db_app, session=self.session), ).grid(row=1, column=1, padx=6, pady=6, sticky="ew") ctk.CTkButton( wrap, text="UDC Fantasma", state="normal" if self.session.can("launcher.open_multi_udc") else "disabled", command=lambda: open_celle_multiple_window(self, db_app, session=self.session), ).grid(row=1, column=2, padx=6, pady=6, sticky="ew") ctk.CTkButton( wrap, text="Ricerca UDC", state="normal" if self.session.can("launcher.open_search") else "disabled", command=lambda: open_search_window(self, db_app, session=self.session), ).grid(row=1, column=3, padx=6, pady=6, sticky="ew") ctk.CTkButton( wrap, text="Gestione Picking List", state="normal" if self.session.can("launcher.open_pickinglist") else "disabled", command=lambda: open_pickinglist_window(self, db_app, session=self.session), ).grid(row=1, column=4, padx=6, pady=6, sticky="ew") for i in range(5): wrap.grid_columnconfigure(i, weight=1) def _on_close(): """Dispose shared resources before closing the launcher.""" try: if self.session is not None: log_session_event(self.session, action="logout", outcome="ok") fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) try: fut.result(timeout=2) except Exception: pass finally: self.destroy() self.protocol("WM_DELETE_WINDOW", _on_close) try: self.lift() self.focus_force() except Exception: pass if __name__ == "__main__": ctk.set_appearance_mode("light") ctk.set_default_color_theme("green") if not _acquire_single_instance_mutex(): root = tk.Tk() root.withdraw() messagebox.showwarning( "Warehouse", "L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.", parent=root, ) try: root.destroy() except Exception: pass raise SystemExit(0) if BYPASS_LOGIN: session = _build_bypass_session() log_session_event( session, action="login.bypass", outcome="ok", details={"login": session.login}, ) bootstrap = None else: bootstrap = tk.Tk() bootstrap.geometry("1x1+0+0") bootstrap.overrideredirect(True) bootstrap.attributes("-alpha", 0.0) bootstrap.deiconify() bootstrap.update_idletasks() session = prompt_login(bootstrap, db_app) if session is None: fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) try: fut.result(timeout=2) except Exception: pass try: if bootstrap is not None: bootstrap.destroy() except Exception: pass raise SystemExit(0) try: if bootstrap is not None: bootstrap.destroy() except Exception: pass Launcher(session).mainloop()