4 Commits

32 changed files with 776 additions and 144 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 threading
import contextlib import contextlib
from version_info import module_version
__version__ = module_version(__name__)
class _LoopHolder: class _LoopHolder:
"""Store the global loop instance and its worker thread.""" """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 import text
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from version_info import module_version
__version__ = module_version(__name__)
try: try:
import pyodbc import pyodbc

View File

@@ -8,7 +8,9 @@ from pathlib import Path
from typing import Any from typing import Any
from user_session import UserSession 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") AUDIT_LOG_PATH = Path(__file__).with_name("warehouse_audit.log")
_LOGGER = logging.getLogger("warehouse_audit") _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 from runtime_support import ensure_stdio, run_with_fatal_log
ensure_stdio("warehouse_barcode") ensure_stdio("warehouse_barcode")

View File

@@ -19,6 +19,9 @@ from barcode_repository import BarcodeRepository
from barcode_service import BarcodeActionResult, BarcodeService, BarcodeViewState from barcode_service import BarcodeActionResult, BarcodeService, BarcodeViewState
from db_config import build_dsn_from_config, ensure_db_config from db_config import build_dsn_from_config, ensure_db_config
from login_window import prompt_login_compact from login_window import prompt_login_compact
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
class BarcodeClientApp: class BarcodeClientApp:
@@ -59,9 +62,11 @@ class BarcodeClientApp:
self._apply_state(self.service.state) self._apply_state(self.service.state)
self._bind_keys() self._bind_keys()
self.root.protocol("WM_DELETE_WINDOW", self._shutdown) 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: def _build_ui(self) -> None:
self.root.title("WMS") self.root.title(versioned_title("WMS", __name__))
self.root.configure(bg="#f1f1f1") self.root.configure(bg="#f1f1f1")
self._apply_responsive_geometry() self._apply_responsive_geometry()
@@ -254,6 +259,18 @@ class BarcodeClientApp:
self.root.geometry(f"{width}x{height}+{x}+{y}") 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: def _bind_keys(self) -> None:
self.root.bind("<F1>", lambda _e: self._start_queue(1)) self.root.bind("<F1>", lambda _e: self._start_queue(1))
self.root.bind("<F2>", lambda _e: self._start_queue(0)) 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 dataclasses import dataclass
from typing import Any from typing import Any
from version_info import module_version
__version__ = module_version(__name__)
SQL_NEXT_PICKING = """ SQL_NEXT_PICKING = """
SELECT TOP (1) SELECT TOP (1)

View File

@@ -6,7 +6,9 @@ from dataclasses import dataclass
from typing import Literal from typing import Literal
from barcode_repository import BarcodeRepository, LegacyMoveResult 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"] 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 import customtkinter as ctk
from ui_theme import theme_color, theme_font, theme_padding, theme_value from ui_theme import theme_color, theme_font, theme_padding, theme_value
from version_info import module_version
__version__ = module_version(__name__)
class InlineBusyOverlay: 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 locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_section, theme_value 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") CONFIG_PATH = Path(__file__).with_name("db_connection.json")
DEFAULT_DB_CONFIG: dict[str, Any] = { DEFAULT_DB_CONFIG: dict[str, Any] = {
@@ -101,7 +104,7 @@ class DatabaseConfigWindow(tk.Toplevel):
self.result_config: dict[str, Any] | None = None self.result_config: dict[str, Any] | None = None
merged = {**DEFAULT_DB_CONFIG, **(initial or {})} 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.geometry(str(theme_value(self._theme, "window_geometry", "520x360")))
self.resizable(False, False) self.resizable(False, False)
try: try:

View File

@@ -19,8 +19,9 @@ from typing import Any, Callable, Optional
import customtkinter as ctk import customtkinter as ctk
from async_loop_singleton import get_global_loop 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: try:
from async_msssql_query import AsyncMSSQLClient # noqa: F401 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 tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_section, theme_value from ui_theme import theme_color, theme_font, theme_section, theme_value
from user_session import UserSession from user_session import UserSession
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try: try:
from loguru import logger from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available 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._theme = theme_section("layout_window", {})
self._locale_catalog = load_locale_catalog() self._locale_catalog = load_locale_catalog()
self._tooltip_catalog = load_tooltip_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"))) self.geometry(str(theme_value(self._theme, "window_geometry", "1200x740")))
minsize = theme_value(self._theme, "window_minsize", [980, 560]) minsize = theme_value(self._theme, "window_minsize", [980, 560])
self.minsize(int(minsize[0]), int(minsize[1])) 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 gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text 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_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 user_session import UserSession
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) === # === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
import asyncio import asyncio
try: try:
@@ -200,7 +211,7 @@ if _MODULE_LOG_ENABLED:
# -------------------- SQL -------------------- # -------------------- SQL --------------------
SQL_PL = """ SQL_PL = """
SELECT 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 Lotto) AS Lotto,
COUNT(DISTINCT Articolo) AS Articolo, COUNT(DISTINCT Articolo) AS Articolo,
COUNT(DISTINCT Descrizione) AS Descrizione, COUNT(DISTINCT Descrizione) AS Descrizione,
@@ -388,13 +399,14 @@ class ScrollTable(ctk.CTkFrame):
self._sort_key: Optional[str] = None self._sort_key: Optional[str] = None
self._sort_reverse = False self._sort_reverse = False
self.total_w = sum(c.width for c in self.columns) self.total_w = sum(c.width for c in self.columns)
self._vbar_visible = False
self.grid_rowconfigure(1, weight=1) self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1)
# header # header
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0) 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="#f3f3f3", self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color=TABLE_HEADER_BG,
height=ROW_H, width=self.total_w) height=ROW_H, width=self.total_w)
self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw", self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw",
width=self.total_w, height=ROW_H) width=self.total_w, height=ROW_H)
@@ -411,7 +423,6 @@ class ScrollTable(ctk.CTkFrame):
# scrollbars # scrollbars
self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview) self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview)
self.xbar = tk.Scrollbar(self, orient="horizontal", command=self._xscroll_both) 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") self.xbar.grid(row=2, column=0, sticky="ew")
# link scroll # link scroll
@@ -433,14 +444,14 @@ class ScrollTable(ctk.CTkFrame):
for w in self.h_inner.winfo_children(): for w in self.h_inner.winfo_children():
w.destroy() 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) height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False) row.pack(fill="x", expand=False)
row.pack_propagate(False) row.pack_propagate(False)
for col in self.columns: for col in self.columns:
holder = ctk.CTkFrame( holder = ctk.CTkFrame(
row, fg_color="#f3f3f3", row, fg_color=TABLE_HEADER_BG,
width=col.width, height=ROW_H, width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR border_width=1, border_color=self.GRID_COLOR
) )
@@ -451,7 +462,7 @@ class ScrollTable(ctk.CTkFrame):
if col.key == self._sort_key: if col.key == self._sort_key:
header_text = f"{col.title} {'' if self._sort_reverse else ''}" 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) lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
if self.on_header_click and col.key != "__check__": 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.""" """Keep the scroll region aligned with the current body content width."""
self.b_canvas.itemconfigure(self.body_window, width=self.total_w) self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
sr = self.b_canvas.bbox("all") sr = self.b_canvas.bbox("all")
content_height = int(sr[3]) if sr else 0
if sr: if sr:
self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3])) self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3]))
else: else:
self.b_canvas.configure(scrollregion=(0,0,self.total_w,0)) 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): def _on_body_configure(self):
"""React to body resize events by syncing dimensions and header scroll.""" """React to body resize events by syncing dimensions and header scroll."""
@@ -510,8 +538,11 @@ class ScrollTable(ctk.CTkFrame):
delta = getattr(event, "delta", 0) delta = getattr(event, "delta", 0)
if delta == 0: if delta == 0:
return return
if not self._vbar_visible:
return "break"
step = -1 if delta > 0 else 1 step = -1 if delta > 0 else 1
self.b_canvas.yview_scroll(step, "units") self.b_canvas.yview_scroll(step, "units")
return "break"
def clear_rows(self): def clear_rows(self):
"""Remove all rendered body rows.""" """Remove all rendered body rows."""
@@ -527,14 +558,15 @@ class ScrollTable(ctk.CTkFrame):
checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None, checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None,
): ):
"""Append one row to the table body.""" """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) height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False) row.pack(fill="x", expand=False)
row.pack_propagate(False) row.pack_propagate(False)
for i, col in enumerate(self.columns): for i, col in enumerate(self.columns):
holder = ctk.CTkFrame( holder = ctk.CTkFrame(
row, fg_color="transparent", row, fg_color=row_bg,
width=col.width, height=ROW_H, width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR border_width=1, border_color=self.GRID_COLOR
) )
@@ -546,10 +578,10 @@ class ScrollTable(ctk.CTkFrame):
cb = checkbox_builder(holder) cb = checkbox_builder(holder)
cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w") cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w")
else: else:
ctk.CTkLabel(holder, text="").pack(fill="both") ctk.CTkLabel(holder, text="", fg_color=row_bg).pack(fill="both")
else: else:
anchor = (anchors[i] if anchors else col.anchor) 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 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.change_theme("light green")
self.detail_sheet.enable_bindings("all") self.detail_sheet.enable_bindings("all")
self.detail_sheet.headers(self._detail_headers(), redraw=False) 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.bind("<ButtonRelease-1>", self._on_detail_sheet_left_click, add="+")
self.detail_sheet.grid(row=0, column=0, sticky="nsew") self.detail_sheet.grid(row=0, column=0, sticky="nsew")
@@ -733,8 +766,10 @@ class GestionePickingListFrame(ctk.CTkFrame):
data, data,
reset_col_positions=True, reset_col_positions=True,
reset_row_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() self.detail_sheet.set_all_column_widths()
def _detail_sort_value(self, row: Dict[str, Any], key: str): def _detail_sort_value(self, row: Dict[str, Any], key: str):
@@ -904,13 +939,25 @@ class GestionePickingListFrame(ctk.CTkFrame):
self.after_idle(_paint) self.after_idle(_paint)
break break
def _reselect_documento_after_reload(self, documento: str): def _reselect_documento_after_reload(self, documento: str) -> bool:
"""(Opzionale) Dopo un reload DB, riseleziona la PL con lo stesso Documento.""" """After a reload, reselect the same document and reload its details."""
for m in self.rows_models: for m in self.rows_models:
if _s(m.pl.get("Documento")) == _s(documento): if _s(m.pl.get("Documento")) == _s(documento):
self._detail_cache.pop(documento, None)
m.set_checked(True) m.set_checked(True)
self.on_row_checked(m, 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 ----- # ----- eventi -----
@_log_call() @_log_call()
@@ -964,6 +1011,8 @@ class GestionePickingListFrame(ctk.CTkFrame):
@_log_call() @_log_call()
def reload_from_db(self, first: bool = False, reselect_documento: str | None = None): def reload_from_db(self, first: bool = False, reselect_documento: str | None = None):
"""Load or reload the picking list summary table from the database.""" """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 self.spinner.start(" Carico…") # spinner ON
async def _job(): async def _job():
_log_sql("SQL_PL", SQL_PL, {}) _log_sql("SQL_PL", SQL_PL, {})
@@ -973,8 +1022,18 @@ class GestionePickingListFrame(ctk.CTkFrame):
_log_dataset("SQL_PL", rows) _log_dataset("SQL_PL", rows)
self._refresh_mid_rows(rows) self._refresh_mid_rows(rows)
if reselect_documento: if reselect_documento:
self.after_idle(lambda doc=reselect_documento: self._reselect_documento_after_reload(doc)) def _reselect_or_clear(doc=reselect_documento):
self.spinner.stop() # spinner OFF 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 # se era il primo load, ripristina il cursore standard
if self._first_loading: if self._first_loading:
try: try:
@@ -1240,7 +1299,7 @@ def open_pickinglist_window(parent: tk.Misc, db_client, session: UserSession | N
win = ctk.CTkToplevel(parent) win = ctk.CTkToplevel(parent)
locale_catalog = load_locale_catalog() 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", {}) theme = theme_section("pickinglist_window", {})
win.geometry(str(theme_value(theme, "window_geometry", "1200x700"))) win.geometry(str(theme_value(theme, "window_geometry", "1200x700")))
minsize = theme_value(theme, "window_minsize", [1000, 560]) 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 busy_overlay import InlineBusyOverlay
from locale_text import load_locale_catalog, text as loc_text 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_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import style_treeview, zebra_tag
from user_session import UserSession from user_session import UserSession
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
try: try:
from loguru import logger from loguru import logger
@@ -457,7 +461,7 @@ class ScaricoDialog(ctk.CTkToplevel):
self._async = AsyncRunner(self) self._async = AsyncRunner(self)
self.rows_tree: ttk.Treeview | None = None 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.resizable(False, False)
self.transient(parent) self.transient(parent)
self.protocol("WM_DELETE_WINDOW", self._close) 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_rowconfigure(0, weight=1)
tree_host.grid_columnconfigure(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( self.rows_tree = ttk.Treeview(
tree_host, tree_host,
columns=("sel", "udc", "last", "diag"), columns=("sel", "udc", "last", "diag"),
@@ -519,6 +519,13 @@ class ScaricoDialog(ctk.CTkToplevel):
style="Scarico.Treeview", style="Scarico.Treeview",
selectmode="none", 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("sel", text="Sel")
self.rows_tree.heading("udc", text=loc_text("scarico.col.udc", catalog=self._locale_catalog, default="UDC")) 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")) 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.last_event_at,
row.diagnostic_note or "", row.diagnostic_note or "",
), ),
tags=(zebra_tag(idx),),
) )
self.update_idletasks() self.update_idletasks()

View File

@@ -6,6 +6,9 @@ import json
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from version_info import module_version
__version__ = module_version(__name__)
_LOCALE_FILE = Path(__file__).with_name("locale.json") _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 locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_section, theme_value from ui_theme import theme_section, theme_value
from user_session import UserSession, create_user_session from user_session import UserSession, create_user_session
from version_info import module_version, versioned_title
__version__ = module_version(__name__)
SQL_LOGIN = """ SQL_LOGIN = """
@@ -64,8 +67,8 @@ class LoginWindow(tk.Toplevel):
self._show_ready_after_id: str | None = None self._show_ready_after_id: str | None = None
self._clear_topmost_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.title(versioned_title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), __name__))
self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x155+0+0"))) self.geometry("170x148+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x170+0+0")))
self.resizable(False, False) self.resizable(False, False)
try: try:
if parent is not None and parent.winfo_viewable(): if parent is not None and parent.winfo_viewable():
@@ -88,53 +91,59 @@ class LoginWindow(tk.Toplevel):
def _build_ui(self) -> None: def _build_ui(self) -> None:
"""Build the compact operator login form.""" """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.pack(fill="both", expand=True)
body.columnconfigure(1, weight=0) body.columnconfigure(1, weight=0)
row_offset = 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 = 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 = 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: if self.compact:
actions = ttk.Frame(body) actions = ttk.Frame(body)
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0)) actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(3, 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))
self._login_button = ttk.Button( self._login_button = ttk.Button(
actions, actions,
text="OK", text="OK",
command=self._on_login, command=self._on_login,
) )
self._login_button.grid(row=0, column=0, sticky="ew") 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( self._cancel_button = ttk.Button(
actions, actions,
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"), text="Annulla",
command=self._on_cancel, 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( self._login_button = ttk.Button(
actions, actions,
text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"), text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"),
command=self._on_login, command=self._on_login,
) )
self._login_button.grid(row=0, column=0, sticky="ew") 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("<Return>", lambda _e: self._on_login())
self.bind("<Escape>", lambda _e: self._on_cancel()) 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 tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_font, theme_section, theme_value from ui_theme import theme_font, theme_section, theme_value
from user_session import UserSession, create_user_session 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 view_celle_multi_udc import open_celle_multiple_window
from window_placement import ( from window_placement import (
cascade_children_below_parent, cascade_children_below_parent,
@@ -43,6 +44,7 @@ from window_placement import (
# Development shortcut: skip the login dialog and boot directly as MAG1. # Development shortcut: skip the login dialog and boot directly as MAG1.
# Set to False when you want to restore normal authentication. # Set to False when you want to restore normal authentication.
BYPASS_LOGIN = False BYPASS_LOGIN = False
__version__ = module_version(__name__)
BYPASS_LOGIN_USER = { BYPASS_LOGIN_USER = {
"operator_id": 4, "operator_id": 4,
"login": "MAG1", "login": "MAG1",
@@ -176,7 +178,7 @@ class Launcher(ctk.CTk):
color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d")) color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d"))
) )
self.title( 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() self._apply_dynamic_geometry()

View File

@@ -10,6 +10,10 @@ from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from version_info import module_version
__version__ = module_version(__name__)
try: try:
from loguru import logger from loguru import logger
except Exception: # pragma: no cover - safety fallback if dependency is missing locally 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 gestione_scarico import move_pallet_async
from locale_text import load_locale_catalog, text as loc_text from locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_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 from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try: try:
from loguru import logger from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available except Exception: # pragma: no cover - fallback used only when Loguru is not available
@@ -275,7 +279,7 @@ class ResetCorsieWindow(ctk.CTkToplevel):
super().__init__(parent) super().__init__(parent)
self._theme = theme_section("reset_corsie", {}) self._theme = theme_section("reset_corsie", {})
self._locale_catalog = load_locale_catalog() 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"))) self.geometry(str(theme_value(self._theme, "window_geometry", "1000x680")))
minsize = theme_value(self._theme, "window_minsize", [880, 560]) minsize = theme_value(self._theme, "window_minsize", [880, 560])
self.minsize(int(minsize[0]), int(minsize[1])) self.minsize(int(minsize[0]), int(minsize[1]))
@@ -298,35 +302,6 @@ class ResetCorsieWindow(ctk.CTkToplevel):
def _setup_tree_style(self): def _setup_tree_style(self):
"""Apply a denser, spreadsheet-like style to the main result grid.""" """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() @_log_call()
def _build_ui(self): def _build_ui(self):
"""Create selectors, summary widgets and the occupied-cell grid.""" """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)), width=int(theme_value(self._theme, "tree_col_num_udc_width", 160)),
anchor=str(theme_value(self._theme, "tree_col_num_udc_anchor", "center")), 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")) style_treeview(
self.tree.tag_configure("even", background=theme_value(self._theme, "tree_row_even_bg", "#f3f6fb")) 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) sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview)
sx = ttk.Scrollbar(mid, orient="horizontal", command=self.tree.xview) 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(): for item in self.tree.get_children():
self.tree.delete(item) self.tree.delete(item)
for idx, (_idc, ubi, n) in enumerate(det_rows): 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,)) self.tree.insert("", "end", values=(ubi, n), tags=(tag,))
except Exception as ex: except Exception as ex:
_MODULE_LOGGER.exception(f"Errore UI refresh reset corsie corsia={corsia}: {ex}") _MODULE_LOGGER.exception(f"Errore UI refresh reset corsie corsia={corsia}: {ex}")

View File

@@ -6,25 +6,42 @@ import sys
import traceback import traceback
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import tempfile
from typing import Callable, TypeVar from typing import Callable, TypeVar
from version_info import module_version
__version__ = module_version(__name__)
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
FATAL_LOG = BASE_DIR / "warehouse_fatal.log" FATAL_LOG = BASE_DIR / "warehouse_fatal.log"
TEMP_DIR = Path(tempfile.gettempdir())
_STDIO_HANDLES = [] _STDIO_HANDLES = []
T = TypeVar("T") 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: def ensure_stdio(app_name: str) -> None:
"""Give ``pythonw`` a real stdout/stderr target before loggers are imported.""" """Give ``pythonw`` a real stdout/stderr target before loggers are imported."""
stamp = datetime.now().strftime("%Y%m%d") stamp = datetime.now().strftime("%Y%m%d")
if sys.stdout is None: 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) _STDIO_HANDLES.append(handle)
sys.stdout = handle sys.stdout = handle
if sys.stderr is None: 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) _STDIO_HANDLES.append(handle)
sys.stderr = handle sys.stderr = handle
@@ -32,12 +49,21 @@ def ensure_stdio(app_name: str) -> None:
def log_fatal(app_name: str, exc: BaseException) -> None: def log_fatal(app_name: str, exc: BaseException) -> None:
"""Write one startup/runtime crash to a persistent log file.""" """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("\n" + "=" * 80 + "\n")
handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}\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__))) 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: def show_fatal_message(app_name: str, exc: BaseException) -> None:
"""Show a best-effort message box for console-less launches.""" """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() root.withdraw()
messagebox.showerror( messagebox.showerror(
app_name, 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, parent=root,
) )
root.destroy() 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.""" """Run an app entry point and persist otherwise invisible ``pythonw`` crashes."""
try: 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: except BaseException as exc:
log_runtime_event(app_name, f"EXCEPTION type={type(exc).__name__} value={exc!r}")
log_fatal(app_name, exc) log_fatal(app_name, exc)
show_fatal_message(app_name, exc) show_fatal_message(app_name, exc)
raise raise

View File

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

View File

@@ -11,53 +11,78 @@ import customtkinter as ctk
from busy_overlay import InlineBusyOverlay from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner from gestione_aree import AsyncRunner
from gestione_scarico import move_pallet_async
from locale_text import load_locale_catalog, text as loc_text 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_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 from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
SQL_STORICO_PL = """ SQL_STORICO_PL = """
WITH base AS ( WITH base AS (
SELECT * SELECT
*,
NULLIF(LTRIM(RTRIM(CAST(Pallet AS varchar(32)))), '') AS PalletKey
FROM dbo.py_XMag_ViewPackingListStorico 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 SELECT
Documento, Documento,
MAX(DataDocumento) AS DataDocumento, MAX(DataDocumento) AS DataDocumento,
MAX(StatoDocumento) AS StatoDocumento, MAX(StatoDocumento) AS StatoDocumento,
MAX(NAZIONE) AS NAZIONE, MAX(NAZIONE) AS NAZIONE,
MAX(CodNazione) AS CodNazione, 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, COUNT(*) AS RigheTotali,
MIN(Ordinamento) AS PrimoOrdine, MIN(Ordinamento) AS PrimoOrdine,
MAX(IDStato) AS IDStato MAX(IDStato) AS IDStato
FROM base FROM base
GROUP BY Documento 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 SELECT
Documento, m.Documento,
DataDocumento, m.DataDocumento,
StatoDocumento, m.StatoDocumento,
NAZIONE, m.NAZIONE,
CodNazione, m.CodNazione,
TotUDC, COALESCE(a.TotUDC, 0) AS TotUDC,
RigheResidue, COALESCE(a.RigheResidue, 0) AS RigheResidue,
RigheSpedite, COALESCE(a.RigheSpedite, 0) AS RigheSpedite,
RigheTotali, m.RigheTotali,
CASE CASE
WHEN StatoDocumento = 'D' THEN 'Chiusa' WHEN m.StatoDocumento = 'D' AND COALESCE(a.RigheResidue, 0) > 0 THEN 'Chiusa ERP con residui'
WHEN RigheResidue = 0 THEN 'Esaurita' WHEN m.StatoDocumento = 'D' THEN 'Chiusa'
WHEN RigheSpedite > 0 THEN 'In corso' WHEN COALESCE(a.RigheResidue, 0) = 0 THEN 'Esaurita'
WHEN COALESCE(a.RigheSpedite, 0) > 0 THEN 'In corso'
ELSE 'Da lavorare' ELSE 'Da lavorare'
END AS StatoOperativo, END AS StatoOperativo,
IDStato, m.IDStato,
PrimoOrdine m.PrimoOrdine
FROM agg FROM meta m
WHERE (:documento IS NULL OR CAST(Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%')) LEFT JOIN agg a ON a.Documento = m.Documento
ORDER BY Documento DESC; WHERE (:documento IS NULL OR CAST(m.Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
ORDER BY m.Documento DESC;
""" """
SQL_STORICO_PL_DETAILS = """ SQL_STORICO_PL_DETAILS = """
@@ -135,8 +160,11 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
self._async = AsyncRunner(self) self._async = AsyncRunner(self)
self._busy = InlineBusyOverlay(self, self._theme) self._busy = InlineBusyOverlay(self, self._theme)
self.var_documento = tk.StringVar() self.var_documento = tk.StringVar()
self._selected_documento: str | None = None
self._selected_stato_operativo: str = ""
self._detail_rows: list[dict[str, Any]] = []
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"))) self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720")))
minsize = theme_value(self._theme, "window_minsize", [980, 560]) minsize = theme_value(self._theme, "window_minsize", [980, 560])
self.minsize(int(minsize[0]), int(minsize[1])) self.minsize(int(minsize[0]), int(minsize[1]))
@@ -158,7 +186,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")), fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")),
) )
top.grid(row=0, column=0, sticky="ew", padx=8, pady=8) top.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
top.grid_columnconfigure(3, weight=1) top.grid_columnconfigure(4, weight=1)
label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)) label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10))
entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10)) entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10))
@@ -176,6 +204,18 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
).grid( ).grid(
row=0, column=2, sticky="w" row=0, column=2, sticky="w"
) )
self.btn_ship_residuals = ctk.CTkButton(
top,
text=loc_text(
"history.picking.button.ship_residuals",
catalog=self._locale_catalog,
default="Versa residui in 7G.1.1",
),
command=self._ship_selected_residuals,
state="disabled",
font=button_font,
)
self.btn_ship_residuals.grid(row=0, column=3, sticky="w", padx=(12, 0))
self.master_tree = self._make_tree( self.master_tree = self._make_tree(
row=1, row=1,
@@ -228,8 +268,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
for col in columns: for col in columns:
tree.heading(col, text=col) tree.heading(col, text=col)
tree.column(col, width=widths.get(col, 100), anchor="w") 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("done", background="#ECECEC")
tree.tag_configure("active", background="#EAF7EA") tree.tag_configure("active", background="#EAF7EA")
tree.tag_configure("warning", background="#FFE3B3")
sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview) sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview)
sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview) sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview)
tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set) tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
@@ -240,12 +282,15 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
def _load_master(self) -> None: def _load_master(self) -> None:
params = {"documento": str(self.var_documento.get() or "").strip() or None} params = {"documento": str(self.var_documento.get() or "").strip() or None}
previous_documento = self._selected_documento
async def _job(): async def _job():
return await self.db_client.query_json(SQL_STORICO_PL, params) return await self.db_client.query_json(SQL_STORICO_PL, params)
def _ok(res): def _ok(res):
self._fill_master(_rows_to_dicts(res)) self._fill_master(_rows_to_dicts(res))
if previous_documento:
self._restore_master_selection(previous_documento)
def _err(ex): def _err(ex):
messagebox.showerror( messagebox.showerror(
@@ -273,9 +318,25 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
def _fill_master(self, rows: list[dict[str, Any]]) -> None: def _fill_master(self, rows: list[dict[str, Any]]) -> None:
self.master_tree.delete(*self.master_tree.get_children("")) self.master_tree.delete(*self.master_tree.get_children(""))
self.detail_tree.delete(*self.detail_tree.get_children("")) self.detail_tree.delete(*self.detail_tree.get_children(""))
self._selected_documento = None
self._selected_stato_operativo = ""
self._detail_rows = []
self._update_residual_button()
for index, row in enumerate(rows): for index, row in enumerate(rows):
stato = str(row.get("StatoOperativo") or "") 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( self.master_tree.insert(
"", "",
"end", "end",
@@ -291,23 +352,32 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
stato, stato,
row.get("IDStato", ""), row.get("IDStato", ""),
), ),
tags=(tag,) if tag else (), tags=merge_tags(zebra_tag(index), tag),
) )
def _on_master_select(self, _event=None) -> None: def _on_master_select(self, _event=None) -> None:
selected = self.master_tree.selection() selected = self.master_tree.selection()
if not selected: if not selected:
self._selected_documento = None
self._selected_stato_operativo = ""
self._detail_rows = []
self._update_residual_button()
return return
values = self.master_tree.item(selected[0], "values") values = self.master_tree.item(selected[0], "values")
if not values: if not values:
return return
documento = values[0] documento = values[0]
self._selected_documento = str(documento)
self._selected_stato_operativo = str(values[7] if len(values) > 7 else "")
self._detail_rows = []
self._update_residual_button()
async def _job(): async def _job():
return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento}) return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento})
def _ok(res): def _ok(res):
self._fill_detail(_rows_to_dicts(res)) self._fill_detail(_rows_to_dicts(res))
self._update_residual_button()
def _err(ex): def _err(ex):
messagebox.showerror( messagebox.showerror(
@@ -334,8 +404,11 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
def _fill_detail(self, rows: list[dict[str, Any]]) -> None: def _fill_detail(self, rows: list[dict[str, Any]]) -> None:
self.detail_tree.delete(*self.detail_tree.get_children("")) self.detail_tree.delete(*self.detail_tree.get_children(""))
for row in rows: self._detail_rows = 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 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( self.detail_tree.insert(
"", "",
"end", "end",
@@ -351,9 +424,120 @@ class StoricoPickingListWindow(ctk.CTkToplevel):
row.get("Ubicazione", ""), row.get("Ubicazione", ""),
row.get("Ordinamento", ""), row.get("Ordinamento", ""),
), ),
tags=("done",) if done else (), tags=merge_tags(zebra_tag(index), tag),
) )
def _update_residual_button(self) -> None:
"""Enable the bulk shipment button only for closed picking lists with residual UDCs."""
enabled = self._selected_stato_operativo == "Chiusa ERP con residui"
try:
self.btn_ship_residuals.configure(state="normal" if enabled else "disabled")
except Exception:
pass
def _restore_master_selection(self, documento: str) -> None:
"""Re-select a document after a reload, when it is still visible."""
for iid in self.master_tree.get_children(""):
values = self.master_tree.item(iid, "values")
if values and str(values[0]) == str(documento):
self.master_tree.selection_set(iid)
self.master_tree.focus(iid)
self.master_tree.see(iid)
self._on_master_select()
return
def _residual_pallets_from_rows(self, rows: list[dict[str, Any]]) -> list[str]:
"""Return distinct residual UDCs that are not already in 7G.1.1."""
pallets: list[str] = []
seen: set[str] = set()
for row in rows:
pallet = str(row.get("Pallet") or "").strip()
if not pallet or pallet in seen:
continue
try:
cella = int(row.get("Cella") or 0)
except Exception:
cella = 0
if cella == 9999:
continue
seen.add(pallet)
pallets.append(pallet)
return pallets
def _operator_login(self) -> str:
"""Return the user recorded on generated warehouse movements."""
login = str(getattr(self.session, "login", "") or "").strip()
return login or "warehouse_ui"
def _ship_selected_residuals(self) -> None:
"""Move all residual UDCs of the selected closed PL to the shipment cell 7G.1.1."""
if self._selected_stato_operativo != "Chiusa ERP con residui" or not self._selected_documento:
return
estimated = self._residual_pallets_from_rows(self._detail_rows)
count_text = str(len(estimated)) if estimated else "le"
if not messagebox.askyesno(
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
(
f"Documento {self._selected_documento}\n\n"
f"Verranno versate in 7G.1.1 {count_text} UDC residue della picking list chiusa.\n"
"L'operazione registra i movimenti nello storico UDC.\n\n"
"Procedere?"
),
parent=self,
):
return
documento = self._selected_documento
utente = self._operator_login()
async def _job():
detail_res = await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento})
detail_rows = _rows_to_dicts(detail_res)
pallets = self._residual_pallets_from_rows(detail_rows)
results: list[dict[str, Any]] = []
for pallet in pallets:
result = await move_pallet_async(
self.db_client,
barcode_pallet=pallet,
target_idcella=9999,
target_barcode_cella="9000000",
utente=utente,
)
results.append(result)
return {"pallets": pallets, "results": results}
def _ok(res):
pallets = res.get("pallets", []) if isinstance(res, dict) else []
results = res.get("results", []) if isinstance(res, dict) else []
moved = sum(1 for row in results if int(row.get("ok") or 0) == 1)
messagebox.showinfo(
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
f"Documento {documento}\nUDC residue trovate: {len(pallets)}\nUDC versate in 7G.1.1: {moved}",
parent=self,
)
self._selected_documento = str(documento)
self._load_master()
def _err(ex):
messagebox.showerror(
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
f"Versamento residui fallito:\n{ex}",
parent=self,
)
self._async.run(
_job(),
_ok,
_err,
busy=self._busy,
message=f"Verso residui PL {documento} in 7G.1.1...",
)
def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc: def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc:
"""Open the picking-list history window.""" """Open the picking-list history window."""

View File

@@ -12,8 +12,12 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text 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_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 from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
SQL_STORICO_UDC = """ SQL_STORICO_UDC = """
WITH direct AS ( WITH direct AS (
@@ -155,7 +159,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
self._busy = InlineBusyOverlay(self, self._theme) self._busy = InlineBusyOverlay(self, self._theme)
self.var_udc = tk.StringVar(value=str(initial_udc or "")) 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"))) self.geometry(str(theme_value(self._theme, "window_geometry", "1100x720")))
minsize = theme_value(self._theme, "window_minsize", [900, 560]) minsize = theme_value(self._theme, "window_minsize", [900, 560])
self.minsize(int(minsize[0]), int(minsize[1])) self.minsize(int(minsize[0]), int(minsize[1]))
@@ -218,6 +222,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
for col in cols: for col in cols:
self.tree.heading(col, text=col) self.tree.heading(col, text=col)
self.tree.column(col, width=90, anchor="w") 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("ID", width=70, anchor="e")
self.tree.column("Tipo", width=55, anchor="center") self.tree.column("Tipo", width=55, anchor="center")
self.tree.column("Rif", width=70, anchor="e") self.tree.column("Rif", width=70, anchor="e")
@@ -280,7 +285,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
value = row.get(name, "") value = row.get(name, "")
return "" if value is None else value return "" if value is None else value
for row in rows: for index, row in enumerate(rows):
tipo = str(row.get("Tipo") or "") tipo = str(row.get("Tipo") or "")
tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else "" tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else ""
self.tree.insert( self.tree.insert(
@@ -299,7 +304,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
_value(row, "ModUtente"), _value(row, "ModUtente"),
_value(row, "ModDataOra"), _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 from pathlib import Path
import tkinter as tk import tkinter as tk
from version_info import module_version
__version__ = module_version(__name__)
_TOOLTIP_FILE = Path(__file__).with_name("tooltip.json") _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": { "login_window": {
"window_geometry": "165x155+0+0", "window_geometry": "165x170+0+0",
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
"overlay_panel_fg_color": ["#f2f2f2", "#353535"], "overlay_panel_fg_color": ["#f2f2f2", "#353535"],
"overlay_panel_corner_radius": 10, "overlay_panel_corner_radius": 10,

View File

@@ -12,6 +12,10 @@ from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from version_info import module_version
__version__ = module_version(__name__)
THEME_PATH = Path(__file__).with_name("ui_theme.json") 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 datetime import datetime
from typing import FrozenSet from typing import FrozenSet
from version_info import module_version
__version__ = module_version(__name__)
ALL_ACTIONS: FrozenSet[str] = frozenset( 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.3",
"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 locale_text import load_locale_catalog, text as loc_text
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_section, theme_value 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 from window_placement import place_window_fullsize_below_parent_later
__version__ = module_version(__name__)
try: try:
from loguru import logger from loguru import logger
except Exception: # pragma: no cover - fallback used only when Loguru is not available 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._theme = theme_section("multi_udc", {})
self._locale_catalog = load_locale_catalog() self._locale_catalog = load_locale_catalog()
self._tooltip_catalog = load_tooltip_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.session = session
self.geometry(str(theme_value(self._theme, "window_geometry", "1100x700"))) self.geometry(str(theme_value(self._theme, "window_geometry", "1100x700")))
minsize = theme_value(self._theme, "window_minsize", [900, 550]) 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("col2", width=250, anchor="w")
self.tree.column("col3", width=120, anchor="w") self.tree.column("col3", width=120, anchor="w")
self.tree.column("col4", width=260, 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) y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set) 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.heading(key, text=title)
self.sum_tbl.column(key, width=width, anchor=anchor) 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) y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview)
x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview) x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview)
self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set) 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.""" """Populate root tree nodes after the aisle query completes."""
rows = _json_obj(res).get("rows", []) rows = _json_obj(res).get("rows", [])
_log_dataset("multi_udc_corsie", rows) _log_dataset("multi_udc_corsie", rows)
for row in rows: for index, row in enumerate(rows):
corsia = row.get("Corsia") corsia = row.get("Corsia")
if not corsia: if not corsia:
continue continue
@@ -672,7 +678,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=self._format_corsia_text(corsia), text=self._format_corsia_text(corsia),
values=("", "", ""), values=("", "", ""),
open=False, open=False,
tags=("corsia",), tags=merge_tags(zebra_tag(index), "corsia"),
) )
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", "")) self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", ""))
@@ -724,7 +730,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
if not rows: if not rows:
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "", "")) self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "", ""))
return return
for row in rows: for index, row in enumerate(rows):
idc = row["IDCella"] idc = row["IDCella"]
ubi = row["Ubicazione"] ubi = row["Ubicazione"]
corsia = row.get("Corsia") corsia = row.get("Corsia")
@@ -741,7 +747,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=label, text=label,
values=(f"IDCella {idc}", "", ""), values=(f"IDCella {idc}", "", ""),
open=False, 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)): 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=("", "", "")) 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_txt = self.tree.item(parent_iid, "values")[0]
idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None 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", "") pallet = row.get("Pallet", "")
desc = row.get("Descrizione", "") desc = row.get("Descrizione", "")
lotto = row.get("Lotto", "") lotto = row.get("Lotto", "")
@@ -813,7 +819,13 @@ class CelleMultipleWindow(ctk.CTkToplevel):
iid=leaf_id, iid=leaf_id,
text=self._format_pallet_text(str(pallet), leaf_id in self.selected_udc_keys), text=self._format_pallet_text(str(pallet), leaf_id in self.selected_udc_keys),
values=(desc, lotto, causale), 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() @_log_call()
@@ -987,11 +999,12 @@ class CelleMultipleWindow(ctk.CTkToplevel):
_log_dataset("multi_udc_riepilogo", rows) _log_dataset("multi_udc_riepilogo", rows)
for item in self.sum_tbl.get_children(): for item in self.sum_tbl.get_children():
self.sum_tbl.delete(item) self.sum_tbl.delete(item)
for row in rows: for index, row in enumerate(rows):
self.sum_tbl.insert( self.sum_tbl.insert(
"", "",
"end", "end",
values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"), 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): 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 from runtime_support import ensure_stdio, run_with_fatal_log
ensure_stdio("warehouse_main") ensure_stdio("warehouse_main")

View File

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