3 Commits

Author SHA1 Message Date
be7ce700d1 Alpha4 polish griglie e login 2026-06-16 15:51:50 +02:00
29900b8b09 Freeze stato gestione picking e storico 2026-06-15 20:56:35 +02:00
8f9957a2db Introdotto versioning visibile ver 1.0 2026-06-05 11:33:47 +02:00
32 changed files with 631 additions and 143 deletions

View File

@@ -0,0 +1,61 @@
# Installazione produzione - Warehouse/FlyWMS bridge
## Ordine consigliato
1. Fare backup del database `Mediseawall`.
2. Copiare il contenuto dello zip in una cartella locale, ad esempio `C:\flywms`.
3. Installare le dipendenze Python:
```bat
python -m pip install -r requirements.txt
```
4. In SSMS, sul database `Mediseawall`, lanciare:
```text
apply_python_parallel_pickinglist_patch.sql
apply_online_history_forms_patch.sql
```
## Cosa fanno gli script
- `apply_python_parallel_pickinglist_patch.sql` crea il ramo SQL Python per gestione picking list, senza modificare le stored procedure C# legacy.
- `apply_online_history_forms_patch.sql` crea le viste Python-only per "Storico Picking List".
- "Storico movimenti UDC" non richiede script dedicati: legge in sola lettura `MagazziniPallet`, `Celle` e `XMag_GiacenzaPalletPlistChiuse`.
## Rollback SQL
Se serve tornare indietro sugli oggetti Python, usare:
```text
rollback_online_history_forms_patch.sql
rollback_python_parallel_pickinglist_patch.sql
```
La tabella `dbo.PyPickingListReservation`, se creata, puo' rimanere anche in caso di rollback perche' il C# legacy non la usa.
## Avvio
Backoffice con console:
```bat
python main.py
```
Backoffice senza console:
```bat
pythonw warehouse.pyw
```
Barcode senza console:
```bat
pythonw barcode.pyw
```
Se si usa un collegamento Windows, impostare anche la cartella "Da" alla cartella dell'applicazione, ad esempio `C:\flywms`.
## File esclusi dal pacchetto
Il pacchetto non include `db_connection.json`, log, cache Python e vecchi zip locali. Alla prima apertura il programma chiedera' la configurazione DB se `db_connection.json` non esiste.

View File

@@ -9,6 +9,10 @@ import asyncio
import threading
import contextlib
from version_info import module_version
__version__ = module_version(__name__)
class _LoopHolder:
"""Store the global loop instance and its worker thread."""

View File

@@ -18,6 +18,9 @@ from typing import Any, Dict, Optional
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool
from version_info import module_version
__version__ = module_version(__name__)
try:
import pyodbc

View File

@@ -8,7 +8,9 @@ from pathlib import Path
from typing import Any
from user_session import UserSession
from version_info import module_version
__version__ = module_version(__name__)
AUDIT_LOG_PATH = Path(__file__).with_name("warehouse_audit.log")
_LOGGER = logging.getLogger("warehouse_audit")

View File

@@ -1,3 +1,9 @@
from pathlib import Path
import os
os.chdir(Path(__file__).resolve().parent)
from runtime_support import ensure_stdio, run_with_fatal_log
ensure_stdio("warehouse_barcode")

View File

@@ -19,6 +19,9 @@ from barcode_repository import BarcodeRepository
from barcode_service import BarcodeActionResult, BarcodeService, BarcodeViewState
from db_config import build_dsn_from_config, ensure_db_config
from login_window import prompt_login_compact
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
class BarcodeClientApp:
@@ -59,9 +62,11 @@ class BarcodeClientApp:
self._apply_state(self.service.state)
self._bind_keys()
self.root.protocol("WM_DELETE_WINDOW", self._shutdown)
self.root.after(80, self._finalize_window_placement)
self.root.after(250, self._finalize_window_placement)
def _build_ui(self) -> None:
self.root.title("WMS")
self.root.title(versioned_title("WMS", __name__))
self.root.configure(bg="#f1f1f1")
self._apply_responsive_geometry()
@@ -254,6 +259,18 @@ class BarcodeClientApp:
self.root.geometry(f"{width}x{height}+{x}+{y}")
def _finalize_window_placement(self) -> None:
"""Let Windows finish frame sizing, then snap the barcode window in place."""
try:
if self._is_barcode_desktop:
self.root.state("zoomed")
else:
self.root.geometry("+0+0")
self.root.update_idletasks()
except Exception:
pass
def _bind_keys(self) -> None:
self.root.bind("<F1>", lambda _e: self._start_queue(1))
self.root.bind("<F2>", lambda _e: self._start_queue(0))

View File

@@ -9,6 +9,10 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from version_info import module_version
__version__ = module_version(__name__)
SQL_NEXT_PICKING = """
SELECT TOP (1)

View File

@@ -6,7 +6,9 @@ from dataclasses import dataclass
from typing import Literal
from barcode_repository import BarcodeRepository, LegacyMoveResult
from version_info import module_version
__version__ = module_version(__name__)
ModeName = Literal["idle", "priority_high", "priority_low", "manual_load", "manual_unload", "confirm"]

View File

@@ -6,6 +6,9 @@ import tkinter as tk
import customtkinter as ctk
from ui_theme import theme_color, theme_font, theme_padding, theme_value
from version_info import module_version
__version__ = module_version(__name__)
class InlineBusyOverlay:

View File

@@ -13,6 +13,9 @@ from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn
from locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_section, theme_value
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
CONFIG_PATH = Path(__file__).with_name("db_connection.json")
DEFAULT_DB_CONFIG: dict[str, Any] = {
@@ -101,7 +104,7 @@ class DatabaseConfigWindow(tk.Toplevel):
self.result_config: dict[str, Any] | None = None
merged = {**DEFAULT_DB_CONFIG, **(initial or {})}
self.title(loc_text("dbconfig.title", catalog=self._locale_catalog, default="Configurazione Database"))
self.title(versioned_title(loc_text("dbconfig.title", catalog=self._locale_catalog, default="Configurazione Database"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "520x360")))
self.resizable(False, False)
try:

View File

@@ -19,8 +19,9 @@ from typing import Any, Callable, Optional
import customtkinter as ctk
from async_loop_singleton import get_global_loop
from version_info import module_version
__VERSION__ = "GestioneAreeFrame v3.3.0-singleloop"
__version__ = module_version(__name__)
try:
from async_msssql_query import AsyncMSSQLClient # noqa: F401

View File

@@ -26,8 +26,11 @@ from tksheet import Sheet, natural_sort_key
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from user_session import UserSession
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try:
from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available
@@ -212,7 +215,7 @@ class LayoutWindow(ctk.CTkToplevel):
self._theme = theme_section("layout_window", {})
self._locale_catalog = load_locale_catalog()
self._tooltip_catalog = load_tooltip_catalog()
self.title(loc_text("layout.title", catalog=self._locale_catalog, default="Layout corsie"))
self.title(versioned_title(loc_text("layout.title", catalog=self._locale_catalog, default="Layout corsie"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "1200x740")))
minsize = theme_value(self._theme, "window_minsize", [980, 560])
self.minsize(int(minsize[0]), int(minsize[1]))

View File

@@ -70,9 +70,20 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import (
TABLE_HEADER_BG,
TABLE_HEADER_FG,
TABLE_ROW_EVEN,
TABLE_ROW_ODD,
apply_tksheet_visual_style,
apply_tksheet_zebra,
)
from user_session import UserSession
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
import asyncio
try:
@@ -200,7 +211,7 @@ if _MODULE_LOG_ENABLED:
# -------------------- SQL --------------------
SQL_PL = """
SELECT
COUNT(DISTINCT Pallet) AS Pallet,
COUNT(DISTINCT NULLIF(LTRIM(RTRIM(CAST(Pallet AS varchar(32)))), '')) AS Pallet,
COUNT(DISTINCT Lotto) AS Lotto,
COUNT(DISTINCT Articolo) AS Articolo,
COUNT(DISTINCT Descrizione) AS Descrizione,
@@ -388,13 +399,14 @@ class ScrollTable(ctk.CTkFrame):
self._sort_key: Optional[str] = None
self._sort_reverse = False
self.total_w = sum(c.width for c in self.columns)
self._vbar_visible = False
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
# header
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0)
self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color="#f3f3f3",
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0, bg=TABLE_HEADER_BG)
self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color=TABLE_HEADER_BG,
height=ROW_H, width=self.total_w)
self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw",
width=self.total_w, height=ROW_H)
@@ -411,7 +423,6 @@ class ScrollTable(ctk.CTkFrame):
# scrollbars
self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview)
self.xbar = tk.Scrollbar(self, orient="horizontal", command=self._xscroll_both)
self.vbar.grid(row=1, column=1, sticky="ns")
self.xbar.grid(row=2, column=0, sticky="ew")
# link scroll
@@ -433,14 +444,14 @@ class ScrollTable(ctk.CTkFrame):
for w in self.h_inner.winfo_children():
w.destroy()
row = ctk.CTkFrame(self.h_inner, fg_color="#f3f3f3",
row = ctk.CTkFrame(self.h_inner, fg_color=TABLE_HEADER_BG,
height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False)
row.pack_propagate(False)
for col in self.columns:
holder = ctk.CTkFrame(
row, fg_color="#f3f3f3",
row, fg_color=TABLE_HEADER_BG,
width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR
)
@@ -451,7 +462,7 @@ class ScrollTable(ctk.CTkFrame):
if col.key == self._sort_key:
header_text = f"{col.title} {'' if self._sort_reverse else ''}"
lbl = ctk.CTkLabel(holder, text=header_text, anchor="w")
lbl = ctk.CTkLabel(holder, text=header_text, anchor="w", text_color=TABLE_HEADER_FG)
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
if self.on_header_click and col.key != "__check__":
@@ -472,10 +483,27 @@ class ScrollTable(ctk.CTkFrame):
"""Keep the scroll region aligned with the current body content width."""
self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
sr = self.b_canvas.bbox("all")
content_height = int(sr[3]) if sr else 0
if sr:
self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3]))
else:
self.b_canvas.configure(scrollregion=(0,0,self.total_w,0))
self._update_vertical_scrollbar(content_height)
def _update_vertical_scrollbar(self, content_height: int):
"""Show the vertical scrollbar only when body rows exceed the visible area."""
try:
visible_height = max(1, int(self.b_canvas.winfo_height()))
except Exception:
visible_height = 1
needs_scroll = content_height > visible_height + 2
if needs_scroll and not self._vbar_visible:
self.vbar.grid(row=1, column=1, sticky="ns")
self._vbar_visible = True
elif not needs_scroll and self._vbar_visible:
self.vbar.grid_remove()
self._vbar_visible = False
self.b_canvas.yview_moveto(0)
def _on_body_configure(self):
"""React to body resize events by syncing dimensions and header scroll."""
@@ -510,8 +538,11 @@ class ScrollTable(ctk.CTkFrame):
delta = getattr(event, "delta", 0)
if delta == 0:
return
if not self._vbar_visible:
return "break"
step = -1 if delta > 0 else 1
self.b_canvas.yview_scroll(step, "units")
return "break"
def clear_rows(self):
"""Remove all rendered body rows."""
@@ -527,14 +558,15 @@ class ScrollTable(ctk.CTkFrame):
checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None,
):
"""Append one row to the table body."""
row = ctk.CTkFrame(self.b_inner, fg_color="transparent",
row_bg = TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD
row = ctk.CTkFrame(self.b_inner, fg_color=row_bg,
height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False)
row.pack_propagate(False)
for i, col in enumerate(self.columns):
holder = ctk.CTkFrame(
row, fg_color="transparent",
row, fg_color=row_bg,
width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR
)
@@ -546,10 +578,10 @@ class ScrollTable(ctk.CTkFrame):
cb = checkbox_builder(holder)
cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w")
else:
ctk.CTkLabel(holder, text="").pack(fill="both")
ctk.CTkLabel(holder, text="", fg_color=row_bg).pack(fill="both")
else:
anchor = (anchors[i] if anchors else col.anchor)
ctk.CTkLabel(holder, text=values[i], anchor=anchor).pack(
ctk.CTkLabel(holder, text=values[i], anchor=anchor, fg_color=row_bg, text_color="#111827").pack(
fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY
)
@@ -693,6 +725,7 @@ class GestionePickingListFrame(ctk.CTkFrame):
self.detail_sheet.change_theme("light green")
self.detail_sheet.enable_bindings("all")
self.detail_sheet.headers(self._detail_headers(), redraw=False)
apply_tksheet_visual_style(self.detail_sheet)
self.detail_sheet.bind("<ButtonRelease-1>", self._on_detail_sheet_left_click, add="+")
self.detail_sheet.grid(row=0, column=0, sticky="nsew")
@@ -733,8 +766,10 @@ class GestionePickingListFrame(ctk.CTkFrame):
data,
reset_col_positions=True,
reset_row_positions=True,
redraw=True,
redraw=False,
)
apply_tksheet_visual_style(self.detail_sheet)
apply_tksheet_zebra(self.detail_sheet, len(data))
self.detail_sheet.set_all_column_widths()
def _detail_sort_value(self, row: Dict[str, Any], key: str):
@@ -904,13 +939,25 @@ class GestionePickingListFrame(ctk.CTkFrame):
self.after_idle(_paint)
break
def _reselect_documento_after_reload(self, documento: str):
"""(Opzionale) Dopo un reload DB, riseleziona la PL con lo stesso Documento."""
def _reselect_documento_after_reload(self, documento: str) -> bool:
"""After a reload, reselect the same document and reload its details."""
for m in self.rows_models:
if _s(m.pl.get("Documento")) == _s(documento):
self._detail_cache.pop(documento, None)
m.set_checked(True)
self.on_row_checked(m, True)
break
return True
return False
def _selected_documento_for_reload(self) -> str | None:
"""Return the document that should survive a toolbar reload."""
selected = self._get_selected_model()
if selected is not None:
documento = _s(selected.pl.get("Documento"))
return documento or None
documento = _s(self.detail_doc)
return documento or None
# ----- eventi -----
@_log_call()
@@ -964,6 +1011,8 @@ class GestionePickingListFrame(ctk.CTkFrame):
@_log_call()
def reload_from_db(self, first: bool = False, reselect_documento: str | None = None):
"""Load or reload the picking list summary table from the database."""
if reselect_documento is None and not first:
reselect_documento = self._selected_documento_for_reload()
self.spinner.start(" Carico…") # spinner ON
async def _job():
_log_sql("SQL_PL", SQL_PL, {})
@@ -973,8 +1022,18 @@ class GestionePickingListFrame(ctk.CTkFrame):
_log_dataset("SQL_PL", rows)
self._refresh_mid_rows(rows)
if reselect_documento:
self.after_idle(lambda doc=reselect_documento: self._reselect_documento_after_reload(doc))
self.spinner.stop() # spinner OFF
def _reselect_or_clear(doc=reselect_documento):
found = self._reselect_documento_after_reload(doc)
if not found:
self.detail_doc = None
self._draw_details_hint()
self.spinner.stop()
self.busy.hide()
self.after_idle(_reselect_or_clear)
else:
self.detail_doc = None
self._draw_details_hint()
self.spinner.stop() # spinner OFF
# se era il primo load, ripristina il cursore standard
if self._first_loading:
try:
@@ -1240,7 +1299,7 @@ def open_pickinglist_window(parent: tk.Misc, db_client, session: UserSession | N
win = ctk.CTkToplevel(parent)
locale_catalog = load_locale_catalog()
win.title(loc_text("picking.title", catalog=locale_catalog, default="Gestione Picking List"))
win.title(versioned_title(loc_text("picking.title", catalog=locale_catalog, default="Gestione Picking List"), __name__))
theme = theme_section("pickinglist_window", {})
win.geometry(str(theme_value(theme, "window_geometry", "1200x700")))
minsize = theme_value(theme, "window_minsize", [1000, 560])

View File

@@ -20,7 +20,11 @@ from audit_log import log_user_action
from busy_overlay import InlineBusyOverlay
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import style_treeview, zebra_tag
from user_session import UserSession
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
try:
from loguru import logger
@@ -457,7 +461,7 @@ class ScaricoDialog(ctk.CTkToplevel):
self._async = AsyncRunner(self)
self.rows_tree: ttk.Treeview | None = None
self.title(loc_text("scarico.title", catalog=self._locale_catalog, default="Scarica {ubicazione}").format(ubicazione=ubicazione))
self.title(versioned_title(loc_text("scarico.title", catalog=self._locale_catalog, default="Scarica {ubicazione}").format(ubicazione=ubicazione), __name__))
self.resizable(False, False)
self.transient(parent)
self.protocol("WM_DELETE_WINDOW", self._close)
@@ -508,10 +512,6 @@ class ScaricoDialog(ctk.CTkToplevel):
tree_host.grid_rowconfigure(0, weight=1)
tree_host.grid_columnconfigure(0, weight=1)
style = ttk.Style(self)
style.configure("Scarico.Treeview", rowheight=28, font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)))
style.configure("Scarico.Treeview.Heading", font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")))
self.rows_tree = ttk.Treeview(
tree_host,
columns=("sel", "udc", "last", "diag"),
@@ -519,6 +519,13 @@ class ScaricoDialog(ctk.CTkToplevel):
style="Scarico.Treeview",
selectmode="none",
)
style_treeview(
self.rows_tree,
style_name="Scarico.Treeview",
rowheight=28,
font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)),
heading_font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")),
)
self.rows_tree.heading("sel", text="Sel")
self.rows_tree.heading("udc", text=loc_text("scarico.col.udc", catalog=self._locale_catalog, default="UDC"))
self.rows_tree.heading("last", text=loc_text("scarico.col.last_insert", catalog=self._locale_catalog, default="Ultimo inserimento"))
@@ -572,6 +579,7 @@ class ScaricoDialog(ctk.CTkToplevel):
row.last_event_at,
row.diagnostic_note or "",
),
tags=(zebra_tag(idx),),
)
self.update_idletasks()

View File

@@ -6,6 +6,9 @@ import json
from functools import lru_cache
from pathlib import Path
from version_info import module_version
__version__ = module_version(__name__)
_LOCALE_FILE = Path(__file__).with_name("locale.json")

View File

@@ -11,6 +11,9 @@ from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_section, theme_value
from user_session import UserSession, create_user_session
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
SQL_LOGIN = """
@@ -64,8 +67,8 @@ class LoginWindow(tk.Toplevel):
self._show_ready_after_id: str | None = None
self._clear_topmost_after_id: str | None = None
self.title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"))
self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x155+0+0")))
self.title(versioned_title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), __name__))
self.geometry("170x148+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x170+0+0")))
self.resizable(False, False)
try:
if parent is not None and parent.winfo_viewable():
@@ -88,53 +91,59 @@ class LoginWindow(tk.Toplevel):
def _build_ui(self) -> None:
"""Build the compact operator login form."""
body = ttk.Frame(self, padding=8 if self.compact else 8)
body = ttk.Frame(self, padding=6 if self.compact else 8)
body.pack(fill="both", expand=True)
body.columnconfigure(1, weight=0)
row_offset = 0
ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=4)
ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=(2, 3))
self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9))
self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=4)
self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=(2, 3))
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=4)
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=(2, 2))
self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9))
self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=4)
self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=(2, 2))
if self.compact:
actions = ttk.Frame(body)
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0))
self._cancel_button = ttk.Button(
actions,
text="Annulla",
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0))
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(3, 0))
self._login_button = ttk.Button(
actions,
text="OK",
command=self._on_login,
)
self._login_button.grid(row=0, column=0, sticky="ew")
else:
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 2))
actions = ttk.Frame(body)
actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(6, 0))
self._cancel_button = ttk.Button(
actions,
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"),
text="Annulla",
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0))
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0))
else:
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(1, 1))
actions = ttk.Frame(body)
actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(3, 0))
self._login_button = ttk.Button(
actions,
text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"),
command=self._on_login,
)
self._login_button.grid(row=0, column=0, sticky="ew")
self._cancel_button = ttk.Button(
actions,
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"),
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0))
for widget in (self.login_entry, self.password_entry, self._login_button, self._cancel_button):
try:
widget.configure(takefocus=True)
except Exception:
pass
self.bind("<Return>", lambda _e: self._on_login())
self.bind("<Escape>", lambda _e: self._on_cancel())

View File

@@ -33,6 +33,7 @@ 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
from version_info import module_version, versioned_title
from view_celle_multi_udc import open_celle_multiple_window
from window_placement import (
cascade_children_below_parent,
@@ -43,6 +44,7 @@ from window_placement import (
# Development shortcut: skip the login dialog and boot directly as MAG1.
# Set to False when you want to restore normal authentication.
BYPASS_LOGIN = False
__version__ = module_version(__name__)
BYPASS_LOGIN_USER = {
"operator_id": 4,
"login": "MAG1",
@@ -176,7 +178,7 @@ class Launcher(ctk.CTk):
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}"
f"{versioned_title(loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse'), __name__)} - {self.session.display_name}"
)
self._apply_dynamic_geometry()

View File

@@ -10,6 +10,10 @@ from functools import wraps
from pathlib import Path
from typing import Any, Dict, List, Optional
from version_info import module_version
__version__ = module_version(__name__)
try:
from loguru import logger
except Exception: # pragma: no cover - safety fallback if dependency is missing locally

View File

@@ -23,9 +23,13 @@ from gestione_aree import AsyncRunner
from gestione_scarico import move_pallet_async
from locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_padding, theme_section, theme_value
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try:
from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available
@@ -275,7 +279,7 @@ class ResetCorsieWindow(ctk.CTkToplevel):
super().__init__(parent)
self._theme = theme_section("reset_corsie", {})
self._locale_catalog = load_locale_catalog()
self.title(loc_text("reset.title", catalog=self._locale_catalog, default="Gestione Corsie - svuotamento celle per corsia"))
self.title(versioned_title(loc_text("reset.title", catalog=self._locale_catalog, default="Gestione Corsie - svuotamento celle per corsia"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "1000x680")))
minsize = theme_value(self._theme, "window_minsize", [880, 560])
self.minsize(int(minsize[0]), int(minsize[1]))
@@ -298,35 +302,6 @@ class ResetCorsieWindow(ctk.CTkToplevel):
def _setup_tree_style(self):
"""Apply a denser, spreadsheet-like style to the main result grid."""
style = ttk.Style(self)
style.configure(
"ResetCorsie.Treeview.Heading",
font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")),
background=theme_value(self._theme, "tree_heading_bg", "#b8c7db"),
foreground=theme_value(self._theme, "tree_heading_fg", "#10243e"),
relief="flat",
padding=theme_padding(self._theme, "tree_heading_padding", (8, 6)),
)
style.map(
"ResetCorsie.Treeview.Heading",
background=[("active", theme_value(self._theme, "tree_heading_bg_active", "#aebfd6"))],
relief=[("pressed", "groove"), ("!pressed", "flat")],
)
style.configure(
"ResetCorsie.Treeview",
font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)),
rowheight=int(theme_value(self._theme, "tree_row_height", 30)),
background=theme_value(self._theme, "tree_body_bg", "#ffffff"),
fieldbackground=theme_value(self._theme, "tree_body_bg", "#ffffff"),
foreground=theme_value(self._theme, "tree_body_fg", "#1f1f1f"),
borderwidth=0,
)
style.map(
"ResetCorsie.Treeview",
background=[("selected", theme_value(self._theme, "tree_selected_bg", "#cfe4ff"))],
foreground=[("selected", theme_value(self._theme, "tree_selected_fg", "#10243e"))],
)
@_log_call()
def _build_ui(self):
"""Create selectors, summary widgets and the occupied-cell grid."""
@@ -411,8 +386,13 @@ class ResetCorsieWindow(ctk.CTkToplevel):
width=int(theme_value(self._theme, "tree_col_num_udc_width", 160)),
anchor=str(theme_value(self._theme, "tree_col_num_udc_anchor", "center")),
)
self.tree.tag_configure("odd", background=theme_value(self._theme, "tree_row_odd_bg", "#ffffff"))
self.tree.tag_configure("even", background=theme_value(self._theme, "tree_row_even_bg", "#f3f6fb"))
style_treeview(
self.tree,
style_name="ResetCorsie.Treeview",
rowheight=int(theme_value(self._theme, "tree_row_height", 30)),
font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)),
heading_font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")),
)
sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview)
sx = ttk.Scrollbar(mid, orient="horizontal", command=self.tree.xview)
@@ -529,7 +509,7 @@ class ResetCorsieWindow(ctk.CTkToplevel):
for item in self.tree.get_children():
self.tree.delete(item)
for idx, (_idc, ubi, n) in enumerate(det_rows):
tag = "even" if idx % 2 else "odd"
tag = zebra_tag(idx)
self.tree.insert("", "end", values=(ubi, n), tags=(tag,))
except Exception as ex:
_MODULE_LOGGER.exception(f"Errore UI refresh reset corsie corsia={corsia}: {ex}")

View File

@@ -6,25 +6,42 @@ import sys
import traceback
from datetime import datetime
from pathlib import Path
import tempfile
from typing import Callable, TypeVar
from version_info import module_version
__version__ = module_version(__name__)
BASE_DIR = Path(__file__).resolve().parent
FATAL_LOG = BASE_DIR / "warehouse_fatal.log"
TEMP_DIR = Path(tempfile.gettempdir())
_STDIO_HANDLES = []
T = TypeVar("T")
def _open_log_file(path: Path):
"""Open a log path, falling back to the user temp directory when needed."""
try:
path.parent.mkdir(parents=True, exist_ok=True)
return path.open("a", encoding="utf-8", buffering=1)
except Exception:
fallback = TEMP_DIR / path.name
fallback.parent.mkdir(parents=True, exist_ok=True)
return fallback.open("a", encoding="utf-8", buffering=1)
def ensure_stdio(app_name: str) -> None:
"""Give ``pythonw`` a real stdout/stderr target before loggers are imported."""
stamp = datetime.now().strftime("%Y%m%d")
if sys.stdout is None:
handle = (BASE_DIR / f"{app_name}_stdout_{stamp}.log").open("a", encoding="utf-8", buffering=1)
handle = _open_log_file(BASE_DIR / f"{app_name}_stdout_{stamp}.log")
_STDIO_HANDLES.append(handle)
sys.stdout = handle
if sys.stderr is None:
handle = (BASE_DIR / f"{app_name}_stderr_{stamp}.log").open("a", encoding="utf-8", buffering=1)
handle = _open_log_file(BASE_DIR / f"{app_name}_stderr_{stamp}.log")
_STDIO_HANDLES.append(handle)
sys.stderr = handle
@@ -32,12 +49,21 @@ def ensure_stdio(app_name: str) -> None:
def log_fatal(app_name: str, exc: BaseException) -> None:
"""Write one startup/runtime crash to a persistent log file."""
with FATAL_LOG.open("a", encoding="utf-8") as handle:
with _open_log_file(FATAL_LOG) as handle:
handle.write("\n" + "=" * 80 + "\n")
handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}\n")
handle.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
def log_runtime_event(app_name: str, message: str) -> None:
"""Write a lightweight launch/shutdown trace for console-less starts."""
safe_name = "".join(ch if ch.isalnum() or ch in ("_", "-") else "_" for ch in app_name.lower())
path = BASE_DIR / f"{safe_name}_launch.log"
with _open_log_file(path) as handle:
handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {message}\n")
def show_fatal_message(app_name: str, exc: BaseException) -> None:
"""Show a best-effort message box for console-less launches."""
@@ -49,7 +75,10 @@ def show_fatal_message(app_name: str, exc: BaseException) -> None:
root.withdraw()
messagebox.showerror(
app_name,
f"Avvio non riuscito.\n\nDettaglio salvato in:\n{FATAL_LOG}\n\n{exc}",
"Avvio non riuscito.\n\n"
f"Controlla i log in:\n{BASE_DIR}\n\n"
f"Se non ci sono, controlla anche:\n{TEMP_DIR}\n\n"
f"{exc}",
parent=root,
)
root.destroy()
@@ -61,8 +90,15 @@ def run_with_fatal_log(app_name: str, func: Callable[[], T]) -> T:
"""Run an app entry point and persist otherwise invisible ``pythonw`` crashes."""
try:
return func()
log_runtime_event(
app_name,
f"START exe={sys.executable!r} version={sys.version.split()[0]} cwd={Path.cwd()} base={BASE_DIR} argv={sys.argv!r}",
)
result = func()
log_runtime_event(app_name, f"RETURN result={result!r}")
return result
except BaseException as exc:
log_runtime_event(app_name, f"EXCEPTION type={type(exc).__name__} value={exc!r}")
log_fatal(app_name, exc)
show_fatal_message(app_name, exc)
raise

View File

@@ -11,9 +11,19 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import (
apply_tksheet_visual_style,
apply_tksheet_zebra,
merge_tags,
style_treeview,
zebra_tag,
)
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
__version__ = module_version(__name__)
try:
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font
@@ -85,7 +95,7 @@ class SearchWindow(ctk.CTkToplevel):
self._theme = theme_section("search_window", {})
self._locale_catalog = load_locale_catalog()
self._tooltip_catalog = load_tooltip_catalog()
self.title(loc_text("search.title", catalog=self._locale_catalog, default="Ricerca UDC/Lotto/Codice"))
self.title(versioned_title(loc_text("search.title", catalog=self._locale_catalog, default="Ricerca UDC/Lotto/Codice"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x720")))
minsize = theme_value(self._theme, "window_minsize", [900, 560])
self.minsize(int(minsize[0]), int(minsize[1]))
@@ -174,17 +184,25 @@ class SearchWindow(ctk.CTkToplevel):
self.use_sheet = False
cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione")
self.tree = ttk.Treeview(wrap, columns=cols, show="headings")
self._style = ttk.Style(self)
try:
self._style.theme_use(self._style.theme_use())
except Exception:
pass
self._style.configure("Search.Treeview", rowheight=22, font=("", 9))
self._style.configure("Search.Treeview.Heading", font=("", 9, "bold"), background="#F3F4F6")
self._style.map("Search.Treeview", background=[("selected", "#DCEBFF")])
self.tree.configure(style="Search.Treeview")
self.tree.tag_configure("even", background="#FFFFFF")
self.tree.tag_configure("odd", background="#F7F9FC")
headings = {
"IDCella": ("IDCella", 90, "e"),
"Ubicazione": ("Ubicazione", 150, "w"),
"UDC": ("UDC / Barcode", 130, "w"),
"Lotto": ("Lotto", 130, "w"),
"Codice": ("Codice prodotto", 150, "w"),
"Descrizione": ("Descrizione prodotto", 340, "w"),
}
for col in cols:
text, width, anchor = headings[col]
self.tree.heading(col, text=text)
self.tree.column(col, width=width, anchor=anchor, stretch=True)
self._style = style_treeview(
self.tree,
style_name="Search.Treeview",
rowheight=22,
font=("", 9),
heading_font=("", 9, "bold"),
)
self.tree.tag_configure("id9999", background="#FFECEC", foreground="#B00020")
sy = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
@@ -209,7 +227,7 @@ class SearchWindow(ctk.CTkToplevel):
is9999 = int(vals[0]) == 9999
except Exception:
is9999 = False
tags = ("id9999", zebra) if is9999 else (zebra,)
tags = merge_tags(zebra, "id9999" if is9999 else "")
self.tree.item(iid, tags=tags)
def _export_xlsx(self):
@@ -414,6 +432,8 @@ class SearchWindow(ctk.CTkToplevel):
hdrs = list(headers)
hdrs[c] = hdrs[c] + arrow
self.sheet.headers(hdrs)
apply_tksheet_visual_style(self.sheet)
apply_tksheet_zebra(self.sheet, len(data))
except Exception:
pass
@@ -449,7 +469,10 @@ class SearchWindow(ctk.CTkToplevel):
for row in rows:
idc, ubi, udc_v, lot_v, cod_v, desc_v = row
data.append([idc, ubi, udc_v, lot_v, cod_v, desc_v])
self.sheet.headers(["IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione"])
apply_tksheet_visual_style(self.sheet)
self.sheet.set_sheet_data(data)
apply_tksheet_zebra(self.sheet, len(data))
self.sheet.set_all_cell_sizes_to_text()
except Exception:
self.use_sheet = False
@@ -458,12 +481,11 @@ class SearchWindow(ctk.CTkToplevel):
self.tree.delete(iid)
for idx, row in enumerate(rows):
idc, ubi, udc_v, lot_v, cod_v, desc_v = row
zebra = "even" if idx % 2 == 0 else "odd"
try:
is9999 = int(idc) == 9999
except Exception:
is9999 = False
tags = ("id9999", zebra) if is9999 else (zebra,)
tags = merge_tags(zebra_tag(idx), "id9999" if is9999 else "")
self.tree.insert("", "end", values=(idc, ubi, udc_v, lot_v, cod_v, desc_v), tags=tags)
if not rows:

View File

@@ -13,51 +13,75 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import merge_tags, style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
SQL_STORICO_PL = """
WITH base AS (
SELECT *
SELECT
*,
NULLIF(LTRIM(RTRIM(CAST(Pallet AS varchar(32)))), '') AS PalletKey
FROM dbo.py_XMag_ViewPackingListStorico
),
agg AS (
pallets AS (
SELECT
Documento,
PalletKey,
MAX(CASE WHEN Cella <> 9999 OR Cella IS NULL THEN 1 ELSE 0 END) AS HasResiduo,
MAX(CASE WHEN Cella = 9999 THEN 1 ELSE 0 END) AS HasSpedito
FROM base
WHERE PalletKey IS NOT NULL
GROUP BY Documento, PalletKey
),
meta AS (
SELECT
Documento,
MAX(DataDocumento) AS DataDocumento,
MAX(StatoDocumento) AS StatoDocumento,
MAX(NAZIONE) AS NAZIONE,
MAX(CodNazione) AS CodNazione,
COUNT(DISTINCT Pallet) AS TotUDC,
SUM(CASE WHEN Cella = 9999 THEN 1 ELSE 0 END) AS RigheSpedite,
SUM(CASE WHEN Cella <> 9999 OR Cella IS NULL THEN 1 ELSE 0 END) AS RigheResidue,
COUNT(*) AS RigheTotali,
MIN(Ordinamento) AS PrimoOrdine,
MAX(IDStato) AS IDStato
FROM base
GROUP BY Documento
),
agg AS (
SELECT
Documento,
COUNT(*) AS TotUDC,
SUM(CASE WHEN p.HasResiduo = 0 AND p.HasSpedito = 1 THEN 1 ELSE 0 END) AS RigheSpedite,
SUM(CASE WHEN p.HasResiduo = 1 THEN 1 ELSE 0 END) AS RigheResidue
FROM pallets p
GROUP BY Documento
)
SELECT
Documento,
DataDocumento,
StatoDocumento,
NAZIONE,
CodNazione,
TotUDC,
RigheResidue,
RigheSpedite,
RigheTotali,
m.Documento,
m.DataDocumento,
m.StatoDocumento,
m.NAZIONE,
m.CodNazione,
COALESCE(a.TotUDC, 0) AS TotUDC,
COALESCE(a.RigheResidue, 0) AS RigheResidue,
COALESCE(a.RigheSpedite, 0) AS RigheSpedite,
m.RigheTotali,
CASE
WHEN StatoDocumento = 'D' THEN 'Chiusa'
WHEN RigheResidue = 0 THEN 'Esaurita'
WHEN RigheSpedite > 0 THEN 'In corso'
WHEN m.StatoDocumento = 'D' AND COALESCE(a.RigheResidue, 0) > 0 THEN 'Chiusa ERP con residui'
WHEN m.StatoDocumento = 'D' THEN 'Chiusa'
WHEN COALESCE(a.RigheResidue, 0) = 0 THEN 'Esaurita'
WHEN COALESCE(a.RigheSpedite, 0) > 0 THEN 'In corso'
ELSE 'Da lavorare'
END AS StatoOperativo,
IDStato,
PrimoOrdine
FROM agg
WHERE (:documento IS NULL OR CAST(Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
ORDER BY Documento DESC;
m.IDStato,
m.PrimoOrdine
FROM meta m
LEFT JOIN agg a ON a.Documento = m.Documento
WHERE (:documento IS NULL OR CAST(m.Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
ORDER BY m.Documento DESC;
"""
SQL_STORICO_PL_DETAILS = """
@@ -136,7 +160,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
self._busy = InlineBusyOverlay(self, self._theme)
self.var_documento = tk.StringVar()
self.title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"))
self.title(versioned_title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720")))
minsize = theme_value(self._theme, "window_minsize", [980, 560])
self.minsize(int(minsize[0]), int(minsize[1]))
@@ -228,8 +252,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
for col in columns:
tree.heading(col, text=col)
tree.column(col, width=widths.get(col, 100), anchor="w")
style_treeview(tree, style_name="HistoryPicking.Treeview", rowheight=24)
tree.tag_configure("done", background="#ECECEC")
tree.tag_configure("active", background="#EAF7EA")
tree.tag_configure("warning", background="#FFE3B3")
sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview)
sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview)
tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
@@ -275,7 +301,19 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
self.detail_tree.delete(*self.detail_tree.get_children(""))
for index, row in enumerate(rows):
stato = str(row.get("StatoOperativo") or "")
tag = "done" if stato in {"Chiusa", "Esaurita"} else "active" if int(row.get("IDStato") or 0) == 1 else ""
is_open_with_shipped = (
str(row.get("StatoDocumento") or "") == "P"
and int(row.get("RigheSpedite") or 0) > 0
)
tag = (
"warning"
if is_open_with_shipped
else "done"
if stato in {"Chiusa", "Esaurita"}
else "active"
if int(row.get("IDStato") or 0) == 1
else ""
)
self.master_tree.insert(
"",
"end",
@@ -291,7 +329,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
stato,
row.get("IDStato", ""),
),
tags=(tag,) if tag else (),
tags=merge_tags(zebra_tag(index), tag),
)
def _on_master_select(self, _event=None) -> None:
@@ -334,8 +372,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
def _fill_detail(self, rows: list[dict[str, Any]]) -> None:
self.detail_tree.delete(*self.detail_tree.get_children(""))
for row in rows:
for index, row in enumerate(rows):
is_open_shipped = str(row.get("StatoDocumento") or "") == "P" and int(row.get("Cella") or 0) == 9999
done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999
tag = "warning" if is_open_shipped else "done" if done else ""
self.detail_tree.insert(
"",
"end",
@@ -351,7 +391,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
row.get("Ubicazione", ""),
row.get("Ordinamento", ""),
),
tags=("done",) if done else (),
tags=merge_tags(zebra_tag(index), tag),
)

View File

@@ -12,8 +12,12 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import merge_tags, style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
SQL_STORICO_UDC = """
WITH direct AS (
@@ -155,7 +159,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
self._busy = InlineBusyOverlay(self, self._theme)
self.var_udc = tk.StringVar(value=str(initial_udc or ""))
self.title(loc_text("history.udc.title", catalog=self._locale_catalog, default="Storico movimenti UDC"))
self.title(versioned_title(loc_text("history.udc.title", catalog=self._locale_catalog, default="Storico movimenti UDC"), __name__))
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x720")))
minsize = theme_value(self._theme, "window_minsize", [900, 560])
self.minsize(int(minsize[0]), int(minsize[1]))
@@ -218,6 +222,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
for col in cols:
self.tree.heading(col, text=col)
self.tree.column(col, width=90, anchor="w")
style_treeview(self.tree, style_name="HistoryUDC.Treeview", rowheight=24)
self.tree.column("ID", width=70, anchor="e")
self.tree.column("Tipo", width=55, anchor="center")
self.tree.column("Rif", width=70, anchor="e")
@@ -280,7 +285,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
value = row.get(name, "")
return "" if value is None else value
for row in rows:
for index, row in enumerate(rows):
tipo = str(row.get("Tipo") or "")
tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else ""
self.tree.insert(
@@ -299,7 +304,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
_value(row, "ModUtente"),
_value(row, "ModDataOra"),
),
tags=(tag,) if tag else (),
tags=merge_tags(zebra_tag(index), tag),
)

View File

@@ -6,6 +6,9 @@ import json
from pathlib import Path
import tkinter as tk
from version_info import module_version
__version__ = module_version(__name__)
_TOOLTIP_FILE = Path(__file__).with_name("tooltip.json")

108
ui_tables.py Normal file
View File

@@ -0,0 +1,108 @@
"""Shared visual helpers for data grids."""
from __future__ import annotations
from tkinter import ttk
from typing import Any
TABLE_ROW_EVEN = "#FFFFFF"
TABLE_ROW_ODD = "#F4F6F8"
TABLE_HEADER_BG = "#D1D5DB"
TABLE_HEADER_FG = "#111827"
TABLE_SELECTED_BG = "#DCEBFF"
TABLE_SELECTED_FG = "#111827"
def style_treeview(
tree: ttk.Treeview,
*,
style_name: str,
rowheight: int = 24,
font: Any = ("Segoe UI", 9),
heading_font: Any = ("Segoe UI", 9, "bold"),
) -> ttk.Style:
"""Apply a consistent high-contrast header and zebra-ready style."""
style = ttk.Style(tree)
style.configure(style_name, rowheight=rowheight, font=font)
style.configure(
f"{style_name}.Heading",
font=heading_font,
background=TABLE_HEADER_BG,
foreground=TABLE_HEADER_FG,
relief="flat",
)
style.map(
f"{style_name}.Heading",
background=[("active", TABLE_HEADER_BG), ("pressed", TABLE_HEADER_BG)],
foreground=[("active", TABLE_HEADER_FG), ("pressed", TABLE_HEADER_FG)],
)
style.map(
style_name,
background=[("selected", TABLE_SELECTED_BG)],
foreground=[("selected", TABLE_SELECTED_FG)],
)
tree.configure(style=style_name)
configure_treeview_zebra_tags(tree)
return style
def configure_treeview_zebra_tags(tree: ttk.Treeview) -> None:
"""Register alternating row color tags on a Treeview."""
tree.tag_configure("even", background=TABLE_ROW_EVEN)
tree.tag_configure("odd", background=TABLE_ROW_ODD)
def zebra_tag(index: int) -> str:
"""Return the alternating row tag for the given zero-based index."""
return "even" if index % 2 == 0 else "odd"
def merge_tags(*tags: str) -> tuple[str, ...]:
"""Return non-empty tags preserving order."""
return tuple(tag for tag in tags if tag)
def apply_tksheet_visual_style(sheet: Any) -> None:
"""Apply best-effort header contrast and zebra rows to a tksheet widget."""
try:
sheet.set_options(
header_bg=TABLE_HEADER_BG,
header_fg=TABLE_HEADER_FG,
header_selected_cells_bg=TABLE_HEADER_BG,
header_selected_cells_fg=TABLE_HEADER_FG,
table_bg=TABLE_ROW_EVEN,
table_fg="#111827",
selected_rows_bg=TABLE_SELECTED_BG,
selected_rows_fg=TABLE_SELECTED_FG,
selected_cells_bg=TABLE_SELECTED_BG,
selected_cells_fg=TABLE_SELECTED_FG,
)
except Exception:
pass
def apply_tksheet_zebra(sheet: Any, row_count: int) -> None:
"""Apply alternating row colors to a tksheet widget when supported."""
try:
sheet.dehighlight_rows(redraw=False)
except Exception:
pass
for row_index in range(row_count):
try:
sheet.highlight_rows(
rows=[row_index],
bg=TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD,
redraw=False,
)
except Exception:
break
try:
sheet.redraw()
except Exception:
pass

View File

@@ -164,7 +164,7 @@
}
},
"login_window": {
"window_geometry": "165x155+0+0",
"window_geometry": "165x170+0+0",
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
"overlay_panel_fg_color": ["#f2f2f2", "#353535"],
"overlay_panel_corner_radius": 10,

View File

@@ -12,6 +12,10 @@ from functools import lru_cache
from pathlib import Path
from typing import Any
from version_info import module_version
__version__ = module_version(__name__)
THEME_PATH = Path(__file__).with_name("ui_theme.json")

View File

@@ -6,6 +6,9 @@ from dataclasses import dataclass, field
from datetime import datetime
from typing import FrozenSet
from version_info import module_version
__version__ = module_version(__name__)
ALL_ACTIONS: FrozenSet[str] = frozenset(
{

71
version_info.py Normal file
View File

@@ -0,0 +1,71 @@
"""Application and module version registry for Warehouse/FlyWMS bridge."""
from __future__ import annotations
from pathlib import Path
APP_VERSION = "1.0.0"
MODULE_VERSIONS: dict[str, str] = {
"app": APP_VERSION,
"async_loop_singleton": "1.0.0",
"async_msssql_query": "1.0.0",
"audit_log": "1.0.0",
"main": "1.0.0",
"barcode_client": "1.0.0",
"barcode_repository": "1.0.0",
"barcode_service": "1.0.0",
"busy_overlay": "1.0.0",
"db_config": "1.0.0",
"gestione_aree": "1.0.0",
"gestione_layout": "1.0.0",
"gestione_pickinglist": "1.0.2",
"gestione_scarico": "1.0.0",
"locale_text": "1.0.0",
"login_window": "1.0.0",
"prenota_sprenota_sql": "1.0.0",
"reset_corsie": "1.0.0",
"search_pallets": "1.0.0",
"storico_pickinglist": "1.0.2",
"storico_udc": "1.0.0",
"tooltips": "1.0.0",
"ui_theme": "1.0.0",
"user_session": "1.0.0",
"view_celle_multi_udc": "1.0.0",
"window_placement": "1.0.0",
}
def module_key(module_name: str) -> str:
"""Return the stable registry key for a module name or path."""
name = str(module_name or "").replace("\\", "/")
stem = Path(name).stem if "/" in name or "." not in name else name.rsplit(".", 1)[-1]
return stem or "app"
def module_version(module_name: str | None = None) -> str:
"""Return the version for a module, falling back to the app version."""
if not module_name:
return APP_VERSION
return MODULE_VERSIONS.get(module_key(module_name), APP_VERSION)
def version_label(module_name: str | None = None) -> str:
"""Return the standard visual label used in window titles."""
return f"ver. {module_version(module_name)}"
def versioned_title(title: str, module_name: str | None = None) -> str:
"""Append the standard version label to a window title."""
clean_title = str(title or "").strip()
label = version_label(module_name)
if not clean_title:
return label
if label.lower() in clean_title.lower():
return clean_title
return f"{clean_title} - {label}"

View File

@@ -22,8 +22,12 @@ from gestione_scarico import move_pallet_async
from locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import merge_tags, style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try:
from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available
@@ -436,7 +440,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
self._theme = theme_section("multi_udc", {})
self._locale_catalog = load_locale_catalog()
self._tooltip_catalog = load_tooltip_catalog()
self.title(loc_text("multi.title", catalog=self._locale_catalog, default="Celle con piu' pallet"))
self.title(versioned_title(loc_text("multi.title", catalog=self._locale_catalog, default="Celle con piu' pallet"), __name__))
self.session = session
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x700")))
minsize = theme_value(self._theme, "window_minsize", [900, 550])
@@ -507,6 +511,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
self.tree.column("col2", width=250, anchor="w")
self.tree.column("col3", width=120, anchor="w")
self.tree.column("col4", width=260, anchor="w")
style_treeview(self.tree, style_name="MultiUDC.Treeview", rowheight=24)
y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set)
@@ -542,6 +547,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
):
self.sum_tbl.heading(key, text=title)
self.sum_tbl.column(key, width=width, anchor=anchor)
style_treeview(self.sum_tbl, style_name="MultiUDCSummary.Treeview", rowheight=24)
y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview)
x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview)
self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set)
@@ -660,7 +666,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
"""Populate root tree nodes after the aisle query completes."""
rows = _json_obj(res).get("rows", [])
_log_dataset("multi_udc_corsie", rows)
for row in rows:
for index, row in enumerate(rows):
corsia = row.get("Corsia")
if not corsia:
continue
@@ -672,7 +678,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=self._format_corsia_text(corsia),
values=("", "", ""),
open=False,
tags=("corsia",),
tags=merge_tags(zebra_tag(index), "corsia"),
)
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", ""))
@@ -724,7 +730,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
if not rows:
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "", ""))
return
for row in rows:
for index, row in enumerate(rows):
idc = row["IDCella"]
ubi = row["Ubicazione"]
corsia = row.get("Corsia")
@@ -741,7 +747,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=label,
values=(f"IDCella {idc}", "", ""),
open=False,
tags=("cella", f"corsia:{corsia}"),
tags=merge_tags(zebra_tag(index), "cella", f"corsia:{corsia}"),
)
if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)):
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", ""))
@@ -781,7 +787,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
idcella_txt = self.tree.item(parent_iid, "values")[0]
idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None
for row in rows:
for index, row in enumerate(rows):
pallet = row.get("Pallet", "")
desc = row.get("Descrizione", "")
lotto = row.get("Lotto", "")
@@ -813,7 +819,13 @@ class CelleMultipleWindow(ctk.CTkToplevel):
iid=leaf_id,
text=self._format_pallet_text(str(pallet), leaf_id in self.selected_udc_keys),
values=(desc, lotto, causale),
tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"),
tags=merge_tags(
zebra_tag(index),
"pallet",
f"corsia:{corsia_val}",
f"ubicazione:{cella_ubi}",
f"idcella:{idcella_num}",
),
)
@_log_call()
@@ -987,11 +999,12 @@ class CelleMultipleWindow(ctk.CTkToplevel):
_log_dataset("multi_udc_riepilogo", rows)
for item in self.sum_tbl.get_children():
self.sum_tbl.delete(item)
for row in rows:
for index, row in enumerate(rows):
self.sum_tbl.insert(
"",
"end",
values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"),
tags=(zebra_tag(index),),
)
def expand_all(self):

View File

@@ -1,3 +1,9 @@
from pathlib import Path
import os
os.chdir(Path(__file__).resolve().parent)
from runtime_support import ensure_stdio, run_with_fatal_log
ensure_stdio("warehouse_main")

View File

@@ -8,6 +8,9 @@ import math
import tkinter as tk
from pathlib import Path
from version_info import module_version
__version__ = module_version(__name__)
MODULE_LOG_NAME = "window_placement"
MODULE_LOG_PATH = Path(__file__).with_name("window_placement.log")