Release storico UDC e picking list

This commit is contained in:
2026-06-03 11:41:25 +02:00
parent 4dabba8ce7
commit 742f6a9fe9
28 changed files with 2021 additions and 42 deletions

121
main.py
View File

@@ -11,6 +11,10 @@ import sys
import tkinter as tk
import time
from runtime_support import ensure_stdio, run_with_fatal_log
ensure_stdio("warehouse_main")
import customtkinter as ctk
from tkinter import messagebox
@@ -24,6 +28,8 @@ from login_window import prompt_login
from locale_text import load_locale_catalog, text as loc_text
from reset_corsie import open_reset_corsie_window
from search_pallets import open_search_window
from storico_pickinglist import open_storico_pickinglist_window
from storico_udc import open_storico_udc_window
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_font, theme_section, theme_value
from user_session import UserSession, create_user_session
@@ -45,9 +51,9 @@ BYPASS_LOGIN_USER = {
"codice_unita": "U1",
}
# Create one global loop and make it the default everywhere.
# Create one global loop for database work. Tk must keep the main thread clean;
# callers schedule async jobs on this loop explicitly.
_loop = get_global_loop()
asyncio.set_event_loop(_loop)
def _noop(*args, **kwargs):
@@ -148,7 +154,9 @@ class Launcher(ctk.CTk):
"layout",
"multi_udc",
"search",
"storico_udc",
"pickinglist",
"storico_pickinglist",
]
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
@@ -164,6 +172,9 @@ class Launcher(ctk.CTk):
self._is_cascading = False
self._focus_restore_pending: set[str] = set()
self._restore_suppressed_until = 0.0
self._exit_icon = self._make_exit_icon(
color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d"))
)
self.title(
f"{loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse 1.0.0')} - {self.session.display_name}"
)
@@ -185,45 +196,63 @@ class Launcher(ctk.CTk):
"reset_corsie",
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
"launcher.open_reset_corsie",
lambda: self._open_child_window(
lambda: self._open_or_focus_child_window(
"reset_corsie",
open_reset_corsie_window(self, self.db_client, session=self.session),
lambda: open_reset_corsie_window(self, self.db_client, session=self.session),
),
),
(
"layout",
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
"launcher.open_layout",
lambda: self._open_child_window(
lambda: self._open_or_focus_child_window(
"layout",
open_layout_window(self, self.db_client, session=self.session),
lambda: open_layout_window(self, self.db_client, session=self.session),
),
),
(
"multi_udc",
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
"launcher.open_multi_udc",
lambda: self._open_child_window(
lambda: self._open_or_focus_child_window(
"multi_udc",
open_celle_multiple_window(self, self.db_client, session=self.session),
lambda: open_celle_multiple_window(self, self.db_client, session=self.session),
),
),
(
"search",
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
"launcher.open_search",
lambda: self._open_child_window(
lambda: self._open_or_focus_child_window(
"search",
open_search_window(self, self.db_client, session=self.session),
lambda: open_search_window(self, self.db_client, session=self.session),
),
),
(
"storico_udc",
loc_text("launcher.history_udc", catalog=self._locale_catalog, default="Storico movimenti UDC"),
"launcher.open_history_udc",
lambda: self._open_or_focus_child_window(
"storico_udc",
lambda: open_storico_udc_window(self, self.db_client, session=self.session),
),
),
(
"pickinglist",
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
"launcher.open_pickinglist",
lambda: self._open_child_window(
lambda: self._open_or_focus_child_window(
"pickinglist",
open_pickinglist_window(self, self.db_client, session=self.session),
lambda: open_pickinglist_window(self, self.db_client, session=self.session),
),
),
(
"storico_pickinglist",
loc_text("launcher.history_pickinglist", catalog=self._locale_catalog, default="Storico Picking List"),
"launcher.open_history_pickinglist",
lambda: self._open_or_focus_child_window(
"storico_pickinglist",
lambda: open_storico_pickinglist_window(self, self.db_client, session=self.session),
),
),
(
@@ -257,12 +286,23 @@ class Launcher(ctk.CTk):
for idx, (_key, label, permission, callback) in enumerate(actions):
row = 1 + (idx // max_buttons_per_row)
column = idx % max_buttons_per_row
text = label
button_options = {}
if _key == "exit":
row = 2
column = max_buttons_per_row - 1
text = label
button_options = {
"image": self._exit_icon,
"compound": "left",
}
button = ctk.CTkButton(
wrap,
text=label,
text=text,
font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
state="normal" if self.session.can(permission) else "disabled",
command=callback,
**button_options,
)
button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
tip = tooltip_text(permission, catalog=self._tooltip_catalog)
@@ -282,6 +322,18 @@ class Launcher(ctk.CTk):
except Exception:
pass
def _make_exit_icon(self, *, color: str) -> tk.PhotoImage:
"""Create a small red X icon without adding image assets to the project."""
size = 14
image = tk.PhotoImage(width=size, height=size)
for offset in range(3, 11):
image.put(color, (offset, offset))
image.put(color, (offset + 1, offset))
image.put(color, (offset, size - 1 - offset))
image.put(color, (offset + 1, size - 1 - offset))
return image
def _apply_dynamic_geometry(self) -> None:
"""Size the launcher around its current content and keep it docked at the top."""
@@ -334,6 +386,34 @@ class Launcher(ctk.CTk):
except Exception:
pass
def _open_or_focus_child_window(self, key: str, factory) -> None:
"""Open one child per launcher key, or focus the existing window."""
self._child_windows_by_key = {
child_key: child
for child_key, child in self._child_windows_by_key.items()
if getattr(child, "winfo_exists", lambda: False)()
}
existing = self._child_windows_by_key.get(key)
if existing is not None and getattr(existing, "winfo_exists", lambda: False)():
try:
if hasattr(existing, "state") and existing.state() == "iconic":
existing.deiconify()
except Exception:
pass
try:
existing.lift()
existing.focus_force()
except Exception:
pass
try:
place_window_below_parent_later(self, existing)
except Exception:
pass
return
self._open_child_window(key, factory())
def _forget_child_window(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None:
"""Remove stale references to child windows that have been closed."""
@@ -460,7 +540,9 @@ class Launcher(ctk.CTk):
self.destroy()
if __name__ == "__main__":
def run_app() -> int:
"""Run the backoffice application entry point."""
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("green")
if not _acquire_single_instance_mutex():
@@ -479,7 +561,7 @@ if __name__ == "__main__":
stop_global_loop()
except Exception:
pass
raise SystemExit(0)
return 0
db_cfg = ensure_db_config(_loop)
if db_cfg is None:
@@ -487,7 +569,7 @@ if __name__ == "__main__":
stop_global_loop()
except Exception:
pass
raise SystemExit(0)
return 0
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
@@ -511,7 +593,7 @@ if __name__ == "__main__":
if session is None:
_shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
raise SystemExit(0)
return 0
_destroy_tk_root(bootstrap)
@@ -519,3 +601,8 @@ if __name__ == "__main__":
Launcher(session, db_app).mainloop()
finally:
_shutdown_runtime(bootstrap=None, dispose_db=True)
return 0
if __name__ == "__main__":
raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))