Files
ware_house/main.py

233 lines
7.5 KiB
Python

"""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()