Checkpoint before ghost pallet cleanup workflow

This commit is contained in:
2026-05-09 12:18:59 +02:00
parent f556b476ff
commit 6ab42a2303
27 changed files with 3947 additions and 973 deletions

210
main.py
View File

@@ -6,38 +6,23 @@ 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 layout_window import open_layout_window
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 view_celle_multiple import open_celle_multiple_window
# Try factory, else frame, else app (senza passare conn_str all'App)
try:
from gestione_pickinglist import create_frame as create_pickinglist_frame
except Exception:
try:
from gestione_pickinglist import GestionePickingListFrame as _PLFrame
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
"""Build the picking list UI using the frame-based fallback."""
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("green")
return _PLFrame(parent, db_client=db_client, conn_str=conn_str)
except Exception:
from gestione_pickinglist import GestionePickingListApp as _PLApp
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
"""Fallback used only by legacy app-style picking list implementations."""
app = _PLApp()
app.mainloop()
return tk.Frame(parent)
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 ----
@@ -46,6 +31,17 @@ 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())
@@ -69,93 +65,91 @@ if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
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 open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient):
"""Open the picking list window while minimizing initial flicker."""
win = ctk.CTkToplevel(parent)
win.title("Gestione Picking List")
win.geometry("1200x700+0+100")
win.minsize(1000, 560)
def _acquire_single_instance_mutex() -> bool:
"""Return ``True`` only for the first running instance of the application."""
# Keep the toplevel hidden while its content is being created.
try:
win.withdraw()
win.attributes("-alpha", 0.0)
except Exception:
pass
global _APP_MUTEX
if not sys.platform.startswith("win"):
return True
frame = create_pickinglist_frame(win, db_client=db_client)
try:
frame.pack(fill="both", expand=True)
except Exception:
pass
kernel32 = ctypes.windll.kernel32
mutex = kernel32.CreateMutexW(None, False, _APP_MUTEX_NAME)
if not mutex:
return True
# Show the window only when the layout is ready.
try:
win.update_idletasks()
try:
win.transient(parent)
except Exception:
pass
try:
win.deiconify()
except Exception:
pass
win.lift()
try:
win.focus_force()
except Exception:
pass
try:
win.attributes("-alpha", 1.0)
except Exception:
pass
except Exception:
pass
last_error = kernel32.GetLastError()
_APP_MUTEX = mutex
ERROR_ALREADY_EXISTS = 183
return last_error != ERROR_ALREADY_EXISTS
win.bind("<Escape>", lambda e: win.destroy())
win.protocol("WM_DELETE_WINDOW", win.destroy)
return win
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):
def __init__(self, session: UserSession):
"""Create the launcher toolbar and wire every button to a feature window."""
super().__init__()
self.title("Warehouse 1.0.0")
self.geometry("1200x70+0+0")
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",
command=lambda: open_reset_corsie_window(self, db_app),
).grid(row=0, column=0, padx=6, pady=6, sticky="ew")
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",
command=lambda: open_layout_window(self, db_app),
).grid(row=0, column=1, padx=6, pady=6, sticky="ew")
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",
command=lambda: open_celle_multiple_window(self, db_app),
).grid(row=0, column=2, padx=6, pady=6, sticky="ew")
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",
command=lambda: open_search_window(self, db_app),
).grid(row=0, column=3, padx=6, pady=6, sticky="ew")
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",
command=lambda: open_pickinglist_window(self, db_app),
).grid(row=0, column=4, padx=6, pady=6, sticky="ew")
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)
@@ -163,6 +157,8 @@ class Launcher(ctk.CTk):
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)
@@ -172,9 +168,65 @@ class Launcher(ctk.CTk):
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")
Launcher().mainloop()
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()