3 Commits

12 changed files with 553 additions and 128 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

@@ -70,6 +70,14 @@ 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 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
@@ -203,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,
@@ -391,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)
@@ -414,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
@@ -436,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
) )
@@ -454,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__":
@@ -475,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."""
@@ -513,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."""
@@ -530,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
) )
@@ -549,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
) )
@@ -696,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")
@@ -736,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):
@@ -907,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()
@@ -967,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, {})
@@ -976,7 +1022,17 @@ 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):
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 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:

View File

@@ -20,6 +20,7 @@ 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 from version_info import module_version, versioned_title
@@ -511,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"),
@@ -522,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"))
@@ -575,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

@@ -68,7 +68,7 @@ class LoginWindow(tk.Toplevel):
self._clear_topmost_after_id: str | None = None self._clear_topmost_after_id: str | None = None
self.title(versioned_title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), __name__)) 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():
@@ -91,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

@@ -23,7 +23,8 @@ 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 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
@@ -301,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."""
@@ -414,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)
@@ -532,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

@@ -11,6 +11,13 @@ 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 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
@@ -177,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)
@@ -212,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):
@@ -417,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
@@ -452,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
@@ -461,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,8 +11,10 @@ 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 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
@@ -21,46 +23,66 @@ __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 = """
@@ -138,6 +160,9 @@ 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(versioned_title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"), __name__)) 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")))
@@ -161,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))
@@ -179,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,
@@ -231,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)
@@ -243,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(
@@ -276,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",
@@ -294,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(
@@ -337,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",
@@ -354,7 +424,118 @@ 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...",
) )

View File

@@ -12,6 +12,7 @@ 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 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
@@ -221,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")
@@ -283,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(
@@ -302,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),
) )

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

@@ -20,14 +20,14 @@ MODULE_VERSIONS: dict[str, str] = {
"db_config": "1.0.0", "db_config": "1.0.0",
"gestione_aree": "1.0.0", "gestione_aree": "1.0.0",
"gestione_layout": "1.0.0", "gestione_layout": "1.0.0",
"gestione_pickinglist": "1.0.0", "gestione_pickinglist": "1.0.2",
"gestione_scarico": "1.0.0", "gestione_scarico": "1.0.0",
"locale_text": "1.0.0", "locale_text": "1.0.0",
"login_window": "1.0.0", "login_window": "1.0.0",
"prenota_sprenota_sql": "1.0.0", "prenota_sprenota_sql": "1.0.0",
"reset_corsie": "1.0.0", "reset_corsie": "1.0.0",
"search_pallets": "1.0.0", "search_pallets": "1.0.0",
"storico_pickinglist": "1.0.0", "storico_pickinglist": "1.0.3",
"storico_udc": "1.0.0", "storico_udc": "1.0.0",
"tooltips": "1.0.0", "tooltips": "1.0.0",
"ui_theme": "1.0.0", "ui_theme": "1.0.0",

View File

@@ -22,6 +22,7 @@ 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 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
@@ -510,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)
@@ -545,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)
@@ -663,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
@@ -675,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=("", "", ""))
@@ -727,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")
@@ -744,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=("", "", ""))
@@ -784,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", "")
@@ -816,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()
@@ -990,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):