2 Commits

Author SHA1 Message Date
be7ce700d1 Alpha4 polish griglie e login 2026-06-16 15:51:50 +02:00
29900b8b09 Freeze stato gestione picking e storico 2026-06-15 20:56:35 +02:00
12 changed files with 408 additions and 127 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 locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import (
TABLE_HEADER_BG,
TABLE_HEADER_FG,
TABLE_ROW_EVEN,
TABLE_ROW_ODD,
apply_tksheet_visual_style,
apply_tksheet_zebra,
)
from user_session import UserSession
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
@@ -203,7 +211,7 @@ if _MODULE_LOG_ENABLED:
# -------------------- SQL --------------------
SQL_PL = """
SELECT
COUNT(DISTINCT Pallet) AS Pallet,
COUNT(DISTINCT NULLIF(LTRIM(RTRIM(CAST(Pallet AS varchar(32)))), '')) AS Pallet,
COUNT(DISTINCT Lotto) AS Lotto,
COUNT(DISTINCT Articolo) AS Articolo,
COUNT(DISTINCT Descrizione) AS Descrizione,
@@ -391,13 +399,14 @@ class ScrollTable(ctk.CTkFrame):
self._sort_key: Optional[str] = None
self._sort_reverse = False
self.total_w = sum(c.width for c in self.columns)
self._vbar_visible = False
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
# header
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0)
self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color="#f3f3f3",
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0, bg=TABLE_HEADER_BG)
self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color=TABLE_HEADER_BG,
height=ROW_H, width=self.total_w)
self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw",
width=self.total_w, height=ROW_H)
@@ -414,7 +423,6 @@ class ScrollTable(ctk.CTkFrame):
# scrollbars
self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview)
self.xbar = tk.Scrollbar(self, orient="horizontal", command=self._xscroll_both)
self.vbar.grid(row=1, column=1, sticky="ns")
self.xbar.grid(row=2, column=0, sticky="ew")
# link scroll
@@ -436,14 +444,14 @@ class ScrollTable(ctk.CTkFrame):
for w in self.h_inner.winfo_children():
w.destroy()
row = ctk.CTkFrame(self.h_inner, fg_color="#f3f3f3",
row = ctk.CTkFrame(self.h_inner, fg_color=TABLE_HEADER_BG,
height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False)
row.pack_propagate(False)
for col in self.columns:
holder = ctk.CTkFrame(
row, fg_color="#f3f3f3",
row, fg_color=TABLE_HEADER_BG,
width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR
)
@@ -454,7 +462,7 @@ class ScrollTable(ctk.CTkFrame):
if col.key == self._sort_key:
header_text = f"{col.title} {'' if self._sort_reverse else ''}"
lbl = ctk.CTkLabel(holder, text=header_text, anchor="w")
lbl = ctk.CTkLabel(holder, text=header_text, anchor="w", text_color=TABLE_HEADER_FG)
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
if self.on_header_click and col.key != "__check__":
@@ -475,10 +483,27 @@ class ScrollTable(ctk.CTkFrame):
"""Keep the scroll region aligned with the current body content width."""
self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
sr = self.b_canvas.bbox("all")
content_height = int(sr[3]) if sr else 0
if sr:
self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3]))
else:
self.b_canvas.configure(scrollregion=(0,0,self.total_w,0))
self._update_vertical_scrollbar(content_height)
def _update_vertical_scrollbar(self, content_height: int):
"""Show the vertical scrollbar only when body rows exceed the visible area."""
try:
visible_height = max(1, int(self.b_canvas.winfo_height()))
except Exception:
visible_height = 1
needs_scroll = content_height > visible_height + 2
if needs_scroll and not self._vbar_visible:
self.vbar.grid(row=1, column=1, sticky="ns")
self._vbar_visible = True
elif not needs_scroll and self._vbar_visible:
self.vbar.grid_remove()
self._vbar_visible = False
self.b_canvas.yview_moveto(0)
def _on_body_configure(self):
"""React to body resize events by syncing dimensions and header scroll."""
@@ -513,8 +538,11 @@ class ScrollTable(ctk.CTkFrame):
delta = getattr(event, "delta", 0)
if delta == 0:
return
if not self._vbar_visible:
return "break"
step = -1 if delta > 0 else 1
self.b_canvas.yview_scroll(step, "units")
return "break"
def clear_rows(self):
"""Remove all rendered body rows."""
@@ -530,14 +558,15 @@ class ScrollTable(ctk.CTkFrame):
checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None,
):
"""Append one row to the table body."""
row = ctk.CTkFrame(self.b_inner, fg_color="transparent",
row_bg = TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD
row = ctk.CTkFrame(self.b_inner, fg_color=row_bg,
height=ROW_H, width=self.total_w)
row.pack(fill="x", expand=False)
row.pack_propagate(False)
for i, col in enumerate(self.columns):
holder = ctk.CTkFrame(
row, fg_color="transparent",
row, fg_color=row_bg,
width=col.width, height=ROW_H,
border_width=1, border_color=self.GRID_COLOR
)
@@ -549,10 +578,10 @@ class ScrollTable(ctk.CTkFrame):
cb = checkbox_builder(holder)
cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w")
else:
ctk.CTkLabel(holder, text="").pack(fill="both")
ctk.CTkLabel(holder, text="", fg_color=row_bg).pack(fill="both")
else:
anchor = (anchors[i] if anchors else col.anchor)
ctk.CTkLabel(holder, text=values[i], anchor=anchor).pack(
ctk.CTkLabel(holder, text=values[i], anchor=anchor, fg_color=row_bg, text_color="#111827").pack(
fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY
)
@@ -696,6 +725,7 @@ class GestionePickingListFrame(ctk.CTkFrame):
self.detail_sheet.change_theme("light green")
self.detail_sheet.enable_bindings("all")
self.detail_sheet.headers(self._detail_headers(), redraw=False)
apply_tksheet_visual_style(self.detail_sheet)
self.detail_sheet.bind("<ButtonRelease-1>", self._on_detail_sheet_left_click, add="+")
self.detail_sheet.grid(row=0, column=0, sticky="nsew")
@@ -736,8 +766,10 @@ class GestionePickingListFrame(ctk.CTkFrame):
data,
reset_col_positions=True,
reset_row_positions=True,
redraw=True,
redraw=False,
)
apply_tksheet_visual_style(self.detail_sheet)
apply_tksheet_zebra(self.detail_sheet, len(data))
self.detail_sheet.set_all_column_widths()
def _detail_sort_value(self, row: Dict[str, Any], key: str):
@@ -907,13 +939,25 @@ class GestionePickingListFrame(ctk.CTkFrame):
self.after_idle(_paint)
break
def _reselect_documento_after_reload(self, documento: str):
"""(Opzionale) Dopo un reload DB, riseleziona la PL con lo stesso Documento."""
def _reselect_documento_after_reload(self, documento: str) -> bool:
"""After a reload, reselect the same document and reload its details."""
for m in self.rows_models:
if _s(m.pl.get("Documento")) == _s(documento):
self._detail_cache.pop(documento, None)
m.set_checked(True)
self.on_row_checked(m, True)
break
return True
return False
def _selected_documento_for_reload(self) -> str | None:
"""Return the document that should survive a toolbar reload."""
selected = self._get_selected_model()
if selected is not None:
documento = _s(selected.pl.get("Documento"))
return documento or None
documento = _s(self.detail_doc)
return documento or None
# ----- eventi -----
@_log_call()
@@ -967,6 +1011,8 @@ class GestionePickingListFrame(ctk.CTkFrame):
@_log_call()
def reload_from_db(self, first: bool = False, reselect_documento: str | None = None):
"""Load or reload the picking list summary table from the database."""
if reselect_documento is None and not first:
reselect_documento = self._selected_documento_for_reload()
self.spinner.start(" Carico…") # spinner ON
async def _job():
_log_sql("SQL_PL", SQL_PL, {})
@@ -976,8 +1022,18 @@ class GestionePickingListFrame(ctk.CTkFrame):
_log_dataset("SQL_PL", rows)
self._refresh_mid_rows(rows)
if reselect_documento:
self.after_idle(lambda doc=reselect_documento: self._reselect_documento_after_reload(doc))
self.spinner.stop() # spinner OFF
def _reselect_or_clear(doc=reselect_documento):
found = self._reselect_documento_after_reload(doc)
if not found:
self.detail_doc = None
self._draw_details_hint()
self.spinner.stop()
self.busy.hide()
self.after_idle(_reselect_or_clear)
else:
self.detail_doc = None
self._draw_details_hint()
self.spinner.stop() # spinner OFF
# se era il primo load, ripristina il cursore standard
if self._first_loading:
try:

View File

@@ -20,6 +20,7 @@ from audit_log import log_user_action
from busy_overlay import InlineBusyOverlay
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import style_treeview, zebra_tag
from user_session import UserSession
from version_info import module_version, versioned_title
@@ -511,10 +512,6 @@ class ScaricoDialog(ctk.CTkToplevel):
tree_host.grid_rowconfigure(0, weight=1)
tree_host.grid_columnconfigure(0, weight=1)
style = ttk.Style(self)
style.configure("Scarico.Treeview", rowheight=28, font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)))
style.configure("Scarico.Treeview.Heading", font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")))
self.rows_tree = ttk.Treeview(
tree_host,
columns=("sel", "udc", "last", "diag"),
@@ -522,6 +519,13 @@ class ScaricoDialog(ctk.CTkToplevel):
style="Scarico.Treeview",
selectmode="none",
)
style_treeview(
self.rows_tree,
style_name="Scarico.Treeview",
rowheight=28,
font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)),
heading_font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")),
)
self.rows_tree.heading("sel", text="Sel")
self.rows_tree.heading("udc", text=loc_text("scarico.col.udc", catalog=self._locale_catalog, default="UDC"))
self.rows_tree.heading("last", text=loc_text("scarico.col.last_insert", catalog=self._locale_catalog, default="Ultimo inserimento"))
@@ -575,6 +579,7 @@ class ScaricoDialog(ctk.CTkToplevel):
row.last_event_at,
row.diagnostic_note or "",
),
tags=(zebra_tag(idx),),
)
self.update_idletasks()

View File

@@ -68,7 +68,7 @@ class LoginWindow(tk.Toplevel):
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.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)
try:
if parent is not None and parent.winfo_viewable():
@@ -91,53 +91,59 @@ class LoginWindow(tk.Toplevel):
def _build_ui(self) -> None:
"""Build the compact operator login form."""
body = ttk.Frame(self, padding=8 if self.compact else 8)
body = ttk.Frame(self, padding=6 if self.compact else 8)
body.pack(fill="both", expand=True)
body.columnconfigure(1, weight=0)
row_offset = 0
ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=4)
ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=(2, 3))
self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9))
self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=4)
self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=(2, 3))
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=4)
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=(2, 2))
self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9))
self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=4)
self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=(2, 2))
if self.compact:
actions = ttk.Frame(body)
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0))
self._cancel_button = ttk.Button(
actions,
text="Annulla",
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0))
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(3, 0))
self._login_button = ttk.Button(
actions,
text="OK",
command=self._on_login,
)
self._login_button.grid(row=0, column=0, sticky="ew")
else:
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 2))
actions = ttk.Frame(body)
actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(6, 0))
self._cancel_button = ttk.Button(
actions,
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"),
text="Annulla",
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0))
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0))
else:
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(1, 1))
actions = ttk.Frame(body)
actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(3, 0))
self._login_button = ttk.Button(
actions,
text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"),
command=self._on_login,
)
self._login_button.grid(row=0, column=0, sticky="ew")
self._cancel_button = ttk.Button(
actions,
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"),
command=self._on_cancel,
)
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0))
for widget in (self.login_entry, self.password_entry, self._login_button, self._cancel_button):
try:
widget.configure(takefocus=True)
except Exception:
pass
self.bind("<Return>", lambda _e: self._on_login())
self.bind("<Escape>", lambda _e: self._on_cancel())

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ from busy_overlay import InlineBusyOverlay
from gestione_aree import AsyncRunner
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import merge_tags, style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
@@ -221,6 +222,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
for col in cols:
self.tree.heading(col, text=col)
self.tree.column(col, width=90, anchor="w")
style_treeview(self.tree, style_name="HistoryUDC.Treeview", rowheight=24)
self.tree.column("ID", width=70, anchor="e")
self.tree.column("Tipo", width=55, anchor="center")
self.tree.column("Rif", width=70, anchor="e")
@@ -283,7 +285,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
value = row.get(name, "")
return "" if value is None else value
for row in rows:
for index, row in enumerate(rows):
tipo = str(row.get("Tipo") or "")
tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else ""
self.tree.insert(
@@ -302,7 +304,7 @@ class StoricoUDCWindow(ctk.CTkToplevel):
_value(row, "ModUtente"),
_value(row, "ModDataOra"),
),
tags=(tag,) if tag else (),
tags=merge_tags(zebra_tag(index), tag),
)

108
ui_tables.py Normal file
View File

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

View File

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

View File

@@ -20,14 +20,14 @@ MODULE_VERSIONS: dict[str, str] = {
"db_config": "1.0.0",
"gestione_aree": "1.0.0",
"gestione_layout": "1.0.0",
"gestione_pickinglist": "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.0",
"storico_pickinglist": "1.0.2",
"storico_udc": "1.0.0",
"tooltips": "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 tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
from ui_tables import merge_tags, style_treeview, zebra_tag
from version_info import module_version, versioned_title
from window_placement import place_window_fullsize_below_parent_later
@@ -510,6 +511,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
self.tree.column("col2", width=250, anchor="w")
self.tree.column("col3", width=120, anchor="w")
self.tree.column("col4", width=260, anchor="w")
style_treeview(self.tree, style_name="MultiUDC.Treeview", rowheight=24)
y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set)
@@ -545,6 +547,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
):
self.sum_tbl.heading(key, text=title)
self.sum_tbl.column(key, width=width, anchor=anchor)
style_treeview(self.sum_tbl, style_name="MultiUDCSummary.Treeview", rowheight=24)
y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview)
x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview)
self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set)
@@ -663,7 +666,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
"""Populate root tree nodes after the aisle query completes."""
rows = _json_obj(res).get("rows", [])
_log_dataset("multi_udc_corsie", rows)
for row in rows:
for index, row in enumerate(rows):
corsia = row.get("Corsia")
if not corsia:
continue
@@ -675,7 +678,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=self._format_corsia_text(corsia),
values=("", "", ""),
open=False,
tags=("corsia",),
tags=merge_tags(zebra_tag(index), "corsia"),
)
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", ""))
@@ -727,7 +730,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
if not rows:
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "", ""))
return
for row in rows:
for index, row in enumerate(rows):
idc = row["IDCella"]
ubi = row["Ubicazione"]
corsia = row.get("Corsia")
@@ -744,7 +747,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
text=label,
values=(f"IDCella {idc}", "", ""),
open=False,
tags=("cella", f"corsia:{corsia}"),
tags=merge_tags(zebra_tag(index), "cella", f"corsia:{corsia}"),
)
if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)):
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", ""))
@@ -784,7 +787,7 @@ class CelleMultipleWindow(ctk.CTkToplevel):
idcella_txt = self.tree.item(parent_iid, "values")[0]
idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None
for row in rows:
for index, row in enumerate(rows):
pallet = row.get("Pallet", "")
desc = row.get("Descrizione", "")
lotto = row.get("Lotto", "")
@@ -816,7 +819,13 @@ class CelleMultipleWindow(ctk.CTkToplevel):
iid=leaf_id,
text=self._format_pallet_text(str(pallet), leaf_id in self.selected_udc_keys),
values=(desc, lotto, causale),
tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"),
tags=merge_tags(
zebra_tag(index),
"pallet",
f"corsia:{corsia_val}",
f"ubicazione:{cella_ubi}",
f"idcella:{idcella_num}",
),
)
@_log_call()
@@ -990,11 +999,12 @@ class CelleMultipleWindow(ctk.CTkToplevel):
_log_dataset("multi_udc_riepilogo", rows)
for item in self.sum_tbl.get_children():
self.sum_tbl.delete(item)
for row in rows:
for index, row in enumerate(rows):
self.sum_tbl.insert(
"",
"end",
values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"),
tags=(zebra_tag(index),),
)
def expand_all(self):