migrazione verso gitea
This commit is contained in:
33
trash/async_runner.py
Normal file
33
trash/async_runner.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Minimal bridge between Tkinter and an external asyncio loop."""
|
||||
|
||||
import asyncio
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class AsyncRunner:
|
||||
"""Run coroutines on a background loop and marshal results back to Tk."""
|
||||
|
||||
def __init__(self, tk_root, loop: asyncio.AbstractEventLoop):
|
||||
"""Store the Tk root widget and the loop used for background work."""
|
||||
self.tk = tk_root
|
||||
self.loop = loop
|
||||
|
||||
def run(self, awaitable, on_ok: Callable, on_err: Callable, busy=None, message: str | None = None):
|
||||
"""Schedule an awaitable and optionally show a busy indicator."""
|
||||
if busy:
|
||||
busy.show(message or "Lavoro in corso...")
|
||||
fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop)
|
||||
self._poll(fut, on_ok, on_err, busy)
|
||||
|
||||
def _poll(self, fut, on_ok, on_err, busy):
|
||||
"""Poll the future until completion and invoke the proper callback."""
|
||||
if fut.done():
|
||||
if busy:
|
||||
busy.hide()
|
||||
try:
|
||||
res = fut.result()
|
||||
on_ok(res)
|
||||
except Exception as ex:
|
||||
on_err(ex)
|
||||
return
|
||||
self.tk.after(50, lambda: self._poll(fut, on_ok, on_err, busy))
|
||||
712
trash/gestione_pickinglist_lento.py
Normal file
712
trash/gestione_pickinglist_lento.py
Normal file
@@ -0,0 +1,712 @@
|
||||
"""Picking list management window.
|
||||
|
||||
The module presents a master/detail UI for packing lists, supports reservation
|
||||
and unreservation through an async stored-procedure port and keeps rendering
|
||||
smooth by relying on deferred updates and lightweight progress indicators.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import tkinter as tk
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
from typing import Optional, Any, Dict, List, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Usa overlay e runner "collaudati"
|
||||
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||
|
||||
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
|
||||
import asyncio
|
||||
try:
|
||||
from prenota_sprenota_sql import sp_xExePackingListPallet_async, SPResult
|
||||
except Exception:
|
||||
async def sp_xExePackingListPallet_async(*args, **kwargs):
|
||||
raise RuntimeError("sp_xExePackingListPallet_async non importabile: verifica prenota_sprenota_sql.py")
|
||||
class SPResult:
|
||||
def __init__(self, rc=-1, message="Procedura non disponibile", id_result=None):
|
||||
self.rc = rc; self.message = message; self.id_result = id_result
|
||||
|
||||
|
||||
# -------------------- SQL --------------------
|
||||
SQL_PL = """
|
||||
SELECT
|
||||
COUNT(DISTINCT Pallet) AS Pallet,
|
||||
COUNT(DISTINCT Lotto) AS Lotto,
|
||||
COUNT(DISTINCT Articolo) AS Articolo,
|
||||
COUNT(DISTINCT Descrizione) AS Descrizione,
|
||||
SUM(Qta) AS Qta,
|
||||
Documento,
|
||||
CodNazione,
|
||||
NAZIONE,
|
||||
Stato,
|
||||
MAX(PalletCella) AS PalletCella,
|
||||
MAX(Magazzino) AS Magazzino,
|
||||
MAX(Area) AS Area,
|
||||
MAX(Cella) AS Cella,
|
||||
MIN(Ordinamento) AS Ordinamento,
|
||||
MAX(IDStato) AS IDStato
|
||||
FROM dbo.XMag_ViewPackingList
|
||||
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
||||
"""
|
||||
|
||||
SQL_PL_DETAILS = """
|
||||
SELECT *
|
||||
FROM ViewPackingListRestante
|
||||
WHERE Documento = :Documento
|
||||
ORDER BY Ordinamento;
|
||||
"""
|
||||
|
||||
# -------------------- helpers --------------------
|
||||
def _rows_to_dicts(res: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Converte il payload ritornato da query_json in lista di dict.
|
||||
Supporta:
|
||||
- res = [ {..}, {..} ]
|
||||
- res = { "rows": [..], "columns": [...] }
|
||||
- res = { "data": [..], "columns": [...] }
|
||||
- res = { "rows": [tuple,..], "columns": [...] }
|
||||
"""
|
||||
if res is None:
|
||||
return []
|
||||
|
||||
if isinstance(res, list):
|
||||
if not res:
|
||||
return []
|
||||
if isinstance(res[0], dict):
|
||||
return res
|
||||
return []
|
||||
|
||||
if isinstance(res, dict):
|
||||
for rows_key in ("rows", "data", "result", "records"):
|
||||
if rows_key in res and isinstance(res[rows_key], list):
|
||||
rows = res[rows_key]
|
||||
if not rows:
|
||||
return []
|
||||
if isinstance(rows[0], dict):
|
||||
return rows
|
||||
cols = res.get("columns") or res.get("cols") or []
|
||||
out = []
|
||||
for r in rows:
|
||||
if cols and isinstance(r, (list, tuple)):
|
||||
out.append({ (cols[i] if i < len(cols) else f"c{i}") : r[i]
|
||||
for i in range(min(len(cols), len(r))) })
|
||||
else:
|
||||
if isinstance(r, (list, tuple)):
|
||||
out.append({ f"c{i}": r[i] for i in range(len(r)) })
|
||||
return out
|
||||
if res and all(not isinstance(v, (list, tuple, dict)) for v in res.values()):
|
||||
return [res]
|
||||
|
||||
return []
|
||||
|
||||
def _s(v) -> str:
|
||||
"""Return a string representation, converting ``None`` to an empty string."""
|
||||
return "" if v is None else str(v)
|
||||
|
||||
def _first(d: Dict[str, Any], keys: List[str], default: str = ""):
|
||||
"""Return the first non-empty value found among the provided keys."""
|
||||
for k in keys:
|
||||
if k in d and d[k] not in (None, ""):
|
||||
return d[k]
|
||||
return default
|
||||
|
||||
# -------------------- column specs --------------------
|
||||
@dataclass
|
||||
class ColSpec:
|
||||
"""Describe one logical column rendered in a ``ScrollTable``."""
|
||||
|
||||
title: str
|
||||
key: str
|
||||
width: int
|
||||
anchor: str # 'w' | 'e' | 'center'
|
||||
|
||||
# Colonne PL (in alto) — include IDStato per la colorazione
|
||||
PL_COLS: List[ColSpec] = [
|
||||
ColSpec("", "__check__", 36, "w"),
|
||||
ColSpec("Documento", "Documento", 120, "w"),
|
||||
ColSpec("NAZIONE", "NAZIONE", 240, "w"),
|
||||
ColSpec("Stato", "Stato", 110, "w"),
|
||||
ColSpec("IDStato", "IDStato", 80, "e"), # nuova colonna
|
||||
ColSpec("#Pallet", "Pallet", 100, "e"),
|
||||
ColSpec("#Lotti", "Lotto", 100, "e"),
|
||||
ColSpec("#Articoli", "Articolo", 110, "e"),
|
||||
ColSpec("Qta", "Qta", 120, "e"),
|
||||
]
|
||||
|
||||
DET_COLS: List[ColSpec] = [
|
||||
ColSpec("UDC/Pallet", "Pallet", 150, "w"),
|
||||
ColSpec("Lotto", "Lotto", 130, "w"),
|
||||
ColSpec("Articolo", "Articolo", 150, "w"),
|
||||
ColSpec("Descrizione","Descrizione", 320, "w"),
|
||||
ColSpec("Qta", "Qta", 110, "e"),
|
||||
ColSpec("Ubicazione", "Ubicazione", 320, "w"),
|
||||
]
|
||||
|
||||
ROW_H = 28
|
||||
|
||||
|
||||
# -------------------- Micro spinner (toolbar) --------------------
|
||||
class ToolbarSpinner:
|
||||
"""
|
||||
Micro-animazione leggerissima per indicare attività:
|
||||
mostra una label con frame: ◐ ◓ ◑ ◒ ... finché è attivo.
|
||||
"""
|
||||
FRAMES = ("◐", "◓", "◑", "◒")
|
||||
def __init__(self, parent: tk.Widget):
|
||||
"""Create the spinner label attached to the given parent widget."""
|
||||
self.parent = parent
|
||||
self.lbl = ctk.CTkLabel(parent, text="", width=28)
|
||||
self._i = 0
|
||||
self._active = False
|
||||
self._job = None
|
||||
|
||||
def widget(self) -> ctk.CTkLabel:
|
||||
"""Return the label widget hosting the spinner animation."""
|
||||
return self.lbl
|
||||
|
||||
def start(self, text: str = ""):
|
||||
"""Start the animation and optionally show a short status message."""
|
||||
if self._active:
|
||||
return
|
||||
self._active = True
|
||||
self.lbl.configure(text=f"{self.FRAMES[self._i]} {text}".strip())
|
||||
self._tick()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the animation and clear the label text."""
|
||||
self._active = False
|
||||
if self._job is not None:
|
||||
try:
|
||||
self.parent.after_cancel(self._job)
|
||||
except Exception:
|
||||
pass
|
||||
self._job = None
|
||||
self.lbl.configure(text="")
|
||||
|
||||
def _tick(self):
|
||||
"""Advance the spinner animation frame."""
|
||||
if not self._active:
|
||||
return
|
||||
self._i = (self._i + 1) % len(self.FRAMES)
|
||||
current = self.lbl.cget("text")
|
||||
# Mantieni l'eventuale testo dopo il simbolo
|
||||
txt_suffix = ""
|
||||
if isinstance(current, str) and len(current) > 2:
|
||||
txt_suffix = current[2:]
|
||||
self.lbl.configure(text=f"{self.FRAMES[self._i]}{txt_suffix}")
|
||||
self._job = self.parent.after(120, self._tick) # 8 fps soft
|
||||
|
||||
|
||||
# -------------------- Scrollable table --------------------
|
||||
class ScrollTable(ctk.CTkFrame):
|
||||
GRID_COLOR = "#D0D5DD"
|
||||
PADX_L = 8
|
||||
PADX_R = 8
|
||||
PADY = 2
|
||||
|
||||
def __init__(self, master, columns: List[ColSpec]):
|
||||
"""Create a fixed-header scrollable table rendered with Tk/CTk widgets."""
|
||||
super().__init__(master)
|
||||
self.columns = columns
|
||||
self.total_w = sum(c.width for c in self.columns)
|
||||
|
||||
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",
|
||||
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)
|
||||
self.h_canvas.grid(row=0, column=0, sticky="ew")
|
||||
|
||||
# body
|
||||
self.b_canvas = tk.Canvas(self, highlightthickness=0, bd=0)
|
||||
self.b_inner = ctk.CTkFrame(self.b_canvas, fg_color="transparent",
|
||||
width=self.total_w)
|
||||
self.body_window = self.b_canvas.create_window((0,0), window=self.b_inner,
|
||||
anchor="nw", width=self.total_w)
|
||||
self.b_canvas.grid(row=1, column=0, sticky="nsew")
|
||||
|
||||
# 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
|
||||
self.b_canvas.configure(yscrollcommand=self.vbar.set, xscrollcommand=self._xscroll_set_both)
|
||||
self.h_canvas.configure(xscrollcommand=self.xbar.set)
|
||||
|
||||
# bind
|
||||
self.h_inner.bind("<Configure>", lambda e: self._sync_header_width())
|
||||
self.b_inner.bind("<Configure>", lambda e: self._on_body_configure())
|
||||
|
||||
self._build_header()
|
||||
|
||||
def _build_header(self):
|
||||
"""Build the static header row using the configured columns."""
|
||||
for w in self.h_inner.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
row = ctk.CTkFrame(self.h_inner, fg_color="#f3f3f3",
|
||||
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",
|
||||
width=col.width, height=ROW_H,
|
||||
border_width=1, border_color=self.GRID_COLOR
|
||||
)
|
||||
holder.pack(side="left", fill="y")
|
||||
holder.pack_propagate(False)
|
||||
|
||||
lbl = ctk.CTkLabel(holder, text=col.title, anchor="w")
|
||||
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
|
||||
|
||||
self.h_inner.configure(width=self.total_w, height=ROW_H)
|
||||
self.h_canvas.configure(scrollregion=(0,0,self.total_w,ROW_H))
|
||||
|
||||
def _update_body_width(self):
|
||||
"""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")
|
||||
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))
|
||||
|
||||
def _on_body_configure(self):
|
||||
"""React to body resize events by syncing dimensions and header scroll."""
|
||||
self._update_body_width()
|
||||
self._sync_header_width()
|
||||
|
||||
def _sync_header_width(self):
|
||||
"""Mirror the body horizontal scroll position on the header canvas."""
|
||||
first, _ = self.b_canvas.xview()
|
||||
self.h_canvas.xview_moveto(first)
|
||||
|
||||
def _xscroll_both(self, *args):
|
||||
"""Scroll header and body together when the horizontal bar moves."""
|
||||
self.h_canvas.xview(*args)
|
||||
self.b_canvas.xview(*args)
|
||||
|
||||
def _xscroll_set_both(self, first, last):
|
||||
"""Update the header viewport and scrollbar thumb in one place."""
|
||||
self.h_canvas.xview_moveto(first)
|
||||
self.xbar.set(first, last)
|
||||
|
||||
def clear_rows(self):
|
||||
"""Remove all rendered body rows."""
|
||||
for w in self.b_inner.winfo_children():
|
||||
w.destroy()
|
||||
self._update_body_width()
|
||||
|
||||
def add_row(
|
||||
self,
|
||||
values: List[str],
|
||||
row_index: int,
|
||||
anchors: Optional[List[str]] = None,
|
||||
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",
|
||||
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",
|
||||
width=col.width, height=ROW_H,
|
||||
border_width=1, border_color=self.GRID_COLOR
|
||||
)
|
||||
holder.pack(side="left", fill="y")
|
||||
holder.pack_propagate(False)
|
||||
|
||||
if col.key == "__check__":
|
||||
if checkbox_builder:
|
||||
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")
|
||||
else:
|
||||
anchor = (anchors[i] if anchors else col.anchor)
|
||||
ctk.CTkLabel(holder, text=values[i], anchor=anchor).pack(
|
||||
fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY
|
||||
)
|
||||
|
||||
self._update_body_width()
|
||||
|
||||
|
||||
# -------------------- PL row model --------------------
|
||||
class PLRow:
|
||||
"""State holder for one picking list row and its selection checkbox."""
|
||||
|
||||
def __init__(self, pl: Dict[str, Any], on_check):
|
||||
"""Bind a picking list payload to a ``BooleanVar`` and callback."""
|
||||
self.pl = pl
|
||||
self.var = ctk.BooleanVar(value=False)
|
||||
self._callback = on_check
|
||||
|
||||
def is_checked(self) -> bool:
|
||||
"""Return whether the row is currently selected."""
|
||||
return self.var.get()
|
||||
|
||||
def set_checked(self, val: bool):
|
||||
"""Programmatically update the checkbox state."""
|
||||
self.var.set(val)
|
||||
|
||||
def build_checkbox(self, parent) -> ctk.CTkCheckBox:
|
||||
"""Create the checkbox widget bound to this row model."""
|
||||
return ctk.CTkCheckBox(parent, text="", variable=self.var,
|
||||
command=lambda: self._callback(self, self.var.get()))
|
||||
|
||||
|
||||
# -------------------- main frame (no-flicker + UX tuning + spinner) --------------------
|
||||
class GestionePickingListFrame(ctk.CTkFrame):
|
||||
def __init__(self, master, *, db_client=None, conn_str=None):
|
||||
"""Create the master/detail picking list frame."""
|
||||
super().__init__(master)
|
||||
if db_client is None:
|
||||
raise ValueError("GestionePickingListFrame richiede un db_client condiviso.")
|
||||
self.db_client = db_client
|
||||
self.runner = AsyncRunner(self) # runner condiviso (usa loop globale)
|
||||
self.busy = BusyOverlay(self) # overlay collaudato
|
||||
|
||||
self.rows_models: list[PLRow] = []
|
||||
self._detail_cache: Dict[Any, list] = {}
|
||||
self.detail_doc = None
|
||||
|
||||
self._first_loading: bool = False # flag per cursore d'attesa solo al primo load
|
||||
|
||||
self._build_layout()
|
||||
# 🔇 Niente reload immediato: carichiamo quando la finestra è idle (= già resa)
|
||||
self.after_idle(self._first_show)
|
||||
|
||||
def _first_show(self):
|
||||
"""Chiamato a finestra già resa → evitiamo sfarfallio del primo paint e mostriamo wait-cursor."""
|
||||
self._first_loading = True
|
||||
try:
|
||||
self.winfo_toplevel().configure(cursor="watch")
|
||||
except Exception:
|
||||
pass
|
||||
# spinner inizia
|
||||
self.spinner.start(" Carico…")
|
||||
self.reload_from_db(first=True)
|
||||
|
||||
# ---------- UI ----------
|
||||
def _build_layout(self):
|
||||
"""Build toolbar, master table and detail table."""
|
||||
for r in (1, 3): self.grid_rowconfigure(r, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
top = ctk.CTkFrame(self)
|
||||
top.grid(row=0, column=0, sticky="ew", padx=10, pady=(8,4))
|
||||
for i, (text, cmd) in enumerate([
|
||||
("Ricarica", self.reload_from_db),
|
||||
("Prenota", self.on_prenota),
|
||||
("S-prenota", self.on_sprenota),
|
||||
("Esporta XLSX", self.on_export)
|
||||
]):
|
||||
ctk.CTkButton(top, text=text, command=cmd).grid(row=0, column=i, padx=6)
|
||||
|
||||
# --- micro spinner a destra della toolbar ---
|
||||
self.spinner = ToolbarSpinner(top)
|
||||
self.spinner.widget().grid(row=0, column=10, padx=(8,0)) # largo spazio a destra
|
||||
|
||||
self.pl_table = ScrollTable(self, PL_COLS)
|
||||
self.pl_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(4,8))
|
||||
|
||||
self.det_table = ScrollTable(self, DET_COLS)
|
||||
self.det_table.grid(row=3, column=0, sticky="nsew", padx=10, pady=(4,10))
|
||||
|
||||
self._draw_details_hint()
|
||||
|
||||
def _draw_details_hint(self):
|
||||
"""Render the placeholder row shown when no document is selected."""
|
||||
self.det_table.clear_rows()
|
||||
self.det_table.add_row(
|
||||
values=["", "", "", "Seleziona una Picking List per vedere le UDC…", "", ""],
|
||||
row_index=0,
|
||||
anchors=["w"]*6
|
||||
)
|
||||
|
||||
def _apply_row_colors(self, rows: List[Dict[str, Any]]):
|
||||
"""Colorazione differita (after_idle) per evitare micro-jank durante l'inserimento righe."""
|
||||
try:
|
||||
for idx, d in enumerate(rows):
|
||||
row_widget = self.pl_table.b_inner.winfo_children()[idx]
|
||||
if int(d.get("IDStato") or 0) == 1:
|
||||
row_widget.configure(fg_color="#ffe6f2") # rosa tenue
|
||||
else:
|
||||
row_widget.configure(fg_color="transparent")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_mid_rows(self, rows: List[Dict[str, Any]]):
|
||||
"""Rebuild the master table using the latest query results."""
|
||||
self.pl_table.clear_rows()
|
||||
self.rows_models.clear()
|
||||
|
||||
for r, d in enumerate(rows):
|
||||
model = PLRow(d, self.on_row_checked)
|
||||
self.rows_models.append(model)
|
||||
values = [
|
||||
"", # checkbox
|
||||
_s(d.get("Documento")),
|
||||
_s(d.get("NAZIONE")),
|
||||
_s(d.get("Stato")),
|
||||
_s(d.get("IDStato")), # nuova colonna visibile
|
||||
_s(d.get("Pallet")),
|
||||
_s(d.get("Lotto")),
|
||||
_s(d.get("Articolo")),
|
||||
_s(d.get("Qta")),
|
||||
]
|
||||
self.pl_table.add_row(
|
||||
values=values,
|
||||
row_index=r,
|
||||
anchors=[c.anchor for c in PL_COLS],
|
||||
checkbox_builder=model.build_checkbox
|
||||
)
|
||||
|
||||
# 🎯 Colora dopo che la UI è resa → no balzi visivi
|
||||
self.after_idle(lambda: self._apply_row_colors(rows))
|
||||
|
||||
# ----- helpers -----
|
||||
def _get_selected_model(self) -> Optional[PLRow]:
|
||||
"""Return the currently checked picking list row, if any."""
|
||||
for m in self.rows_models:
|
||||
if m.is_checked():
|
||||
return m
|
||||
return None
|
||||
|
||||
def _recolor_row_by_documento(self, documento: str, idstato: int):
|
||||
"""Aggiorna colore riga e cella IDStato per il Documento indicato."""
|
||||
for idx, m in enumerate(self.rows_models):
|
||||
if _s(m.pl.get("Documento")) == _s(documento):
|
||||
m.pl["IDStato"] = idstato
|
||||
def _paint():
|
||||
try:
|
||||
row_widget = self.pl_table.b_inner.winfo_children()[idx]
|
||||
row_widget.configure(fg_color="#ffe6f2" if idstato == 1 else "transparent")
|
||||
row_children = row_widget.winfo_children()
|
||||
if len(row_children) >= 5:
|
||||
holder = row_children[4]
|
||||
if holder.winfo_children():
|
||||
lbl = holder.winfo_children()[0]
|
||||
if hasattr(lbl, "configure"):
|
||||
lbl.configure(text=str(idstato))
|
||||
except Exception:
|
||||
pass
|
||||
# differisci la colorazione (smooth)
|
||||
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."""
|
||||
for m in self.rows_models:
|
||||
if _s(m.pl.get("Documento")) == _s(documento):
|
||||
m.set_checked(True)
|
||||
self.on_row_checked(m, True)
|
||||
break
|
||||
|
||||
# ----- eventi -----
|
||||
def on_row_checked(self, model: PLRow, is_checked: bool):
|
||||
"""Handle row selection changes and refresh the detail section."""
|
||||
# selezione esclusiva
|
||||
if is_checked:
|
||||
for m in self.rows_models:
|
||||
if m is not model and m.is_checked():
|
||||
m.set_checked(False)
|
||||
|
||||
self.detail_doc = model.pl.get("Documento")
|
||||
self.spinner.start(" Carico dettagli…") # spinner ON
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc})
|
||||
|
||||
def _ok(res):
|
||||
self.spinner.stop() # spinner OFF
|
||||
self._detail_cache[self.detail_doc] = _rows_to_dicts(res)
|
||||
# differisci il render dei dettagli (più fluido)
|
||||
self.after_idle(self._refresh_details)
|
||||
|
||||
def _err(ex):
|
||||
self.spinner.stop()
|
||||
messagebox.showerror("DB", f"Errore nel caricamento dettagli:\n{ex}")
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=_ok,
|
||||
on_error=_err,
|
||||
busy=self.busy,
|
||||
message=f"Carico UDC per Documento {self.detail_doc}…"
|
||||
)
|
||||
|
||||
else:
|
||||
if not any(m.is_checked() for m in self.rows_models):
|
||||
self.detail_doc = None
|
||||
self._refresh_details()
|
||||
|
||||
# ----- load PL -----
|
||||
def reload_from_db(self, first: bool = False):
|
||||
"""Load or reload the picking list summary table from the database."""
|
||||
self.spinner.start(" Carico…") # spinner ON
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_PL, {})
|
||||
def _on_success(res):
|
||||
rows = _rows_to_dicts(res)
|
||||
self._refresh_mid_rows(rows)
|
||||
self.spinner.stop() # spinner OFF
|
||||
# se era il primo load, ripristina il cursore standard
|
||||
if self._first_loading:
|
||||
try:
|
||||
self.winfo_toplevel().configure(cursor="")
|
||||
except Exception:
|
||||
pass
|
||||
self._first_loading = False
|
||||
def _on_error(ex):
|
||||
self.spinner.stop()
|
||||
if self._first_loading:
|
||||
try:
|
||||
self.winfo_toplevel().configure(cursor="")
|
||||
except Exception:
|
||||
pass
|
||||
self._first_loading = False
|
||||
messagebox.showerror("DB", f"Errore nel caricamento:\n{ex}")
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=_on_success,
|
||||
on_error=_on_error,
|
||||
busy=self.busy,
|
||||
message="Caricamento Picking List…" if first else "Aggiornamento…"
|
||||
)
|
||||
|
||||
def _refresh_details(self):
|
||||
"""Render the detail table for the currently selected document."""
|
||||
self.det_table.clear_rows()
|
||||
if not self.detail_doc:
|
||||
self._draw_details_hint()
|
||||
return
|
||||
|
||||
rows = self._detail_cache.get(self.detail_doc, [])
|
||||
if not rows:
|
||||
self.det_table.add_row(values=["", "", "", "Nessuna UDC trovata.", "", ""],
|
||||
row_index=0, anchors=["w"]*6)
|
||||
return
|
||||
|
||||
for r, d in enumerate(rows):
|
||||
pallet = _s(_first(d, ["Pallet", "UDC", "PalletID"]))
|
||||
lotto = _s(_first(d, ["Lotto"]))
|
||||
articolo = _s(_first(d, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"]))
|
||||
descr = _s(_first(d, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"]))
|
||||
qta = _s(_first(d, ["Qta", "Quantita", "Qty", "QTY"]))
|
||||
ubi_raw = _first(d, ["Ubicazione", "Cella", "PalletCella"])
|
||||
loc = "Non scaffalata" if (ubi_raw is None or str(ubi_raw).strip()=="") else str(ubi_raw).strip()
|
||||
|
||||
self.det_table.add_row(
|
||||
values=[pallet, lotto, articolo, descr, qta, loc],
|
||||
row_index=r,
|
||||
anchors=[c.anchor for c in DET_COLS]
|
||||
)
|
||||
|
||||
# ----- azioni -----
|
||||
def on_prenota(self):
|
||||
"""Reserve the selected picking list."""
|
||||
model = self._get_selected_model()
|
||||
if not model:
|
||||
messagebox.showinfo("Prenota", "Seleziona una Picking List (checkbox) prima di prenotare.")
|
||||
return
|
||||
|
||||
documento = _s(model.pl.get("Documento"))
|
||||
current = int(model.pl.get("IDStato") or 0)
|
||||
desired = 1
|
||||
if current == desired:
|
||||
messagebox.showinfo("Prenota", f"La Picking List {documento} è già prenotata.")
|
||||
return
|
||||
|
||||
id_operatore = 1 # TODO: recupera dal contesto reale
|
||||
self.spinner.start(" Prenoto…")
|
||||
|
||||
async def _job():
|
||||
return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento)
|
||||
|
||||
def _ok(res: SPResult):
|
||||
self.spinner.stop()
|
||||
if res and res.rc == 0:
|
||||
self._recolor_row_by_documento(documento, desired)
|
||||
else:
|
||||
msg = (res.message if res else "Errore sconosciuto")
|
||||
messagebox.showerror("Prenota", f"Operazione non riuscita:\n{msg}")
|
||||
|
||||
def _err(ex):
|
||||
self.spinner.stop()
|
||||
messagebox.showerror("Prenota", f"Errore:\n{ex}")
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=_ok,
|
||||
on_error=_err,
|
||||
busy=self.busy,
|
||||
message=f"Prenoto la Picking List {documento}…"
|
||||
)
|
||||
|
||||
def on_sprenota(self):
|
||||
"""Unreserve the selected picking list."""
|
||||
model = self._get_selected_model()
|
||||
if not model:
|
||||
messagebox.showinfo("S-prenota", "Seleziona una Picking List (checkbox) prima di s-prenotare.")
|
||||
return
|
||||
|
||||
documento = _s(model.pl.get("Documento"))
|
||||
current = int(model.pl.get("IDStato") or 0)
|
||||
desired = 0
|
||||
if current == desired:
|
||||
messagebox.showinfo("S-prenota", f"La Picking List {documento} è già NON prenotata.")
|
||||
return
|
||||
|
||||
id_operatore = 1 # TODO: recupera dal contesto reale
|
||||
self.spinner.start(" S-prenoto…")
|
||||
|
||||
async def _job():
|
||||
return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento)
|
||||
|
||||
def _ok(res: SPResult):
|
||||
self.spinner.stop()
|
||||
if res and res.rc == 0:
|
||||
self._recolor_row_by_documento(documento, desired)
|
||||
else:
|
||||
msg = (res.message if res else "Errore sconosciuto")
|
||||
messagebox.showerror("S-prenota", f"Operazione non riuscita:\n{msg}")
|
||||
|
||||
def _err(ex):
|
||||
self.spinner.stop()
|
||||
messagebox.showerror("S-prenota", f"Errore:\n{ex}")
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=_ok,
|
||||
on_error=_err,
|
||||
busy=self.busy,
|
||||
message=f"S-prenoto la Picking List {documento}…"
|
||||
)
|
||||
|
||||
def on_export(self):
|
||||
"""Placeholder for a future export implementation."""
|
||||
messagebox.showinfo("Esporta", "Stub esportazione.")
|
||||
|
||||
|
||||
# factory per main
|
||||
def create_frame(parent, *, db_client=None, conn_str=None) -> 'GestionePickingListFrame':
|
||||
"""Factory used by the launcher to build the picking list frame."""
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_default_color_theme("green")
|
||||
return GestionePickingListFrame(parent, db_client=db_client)
|
||||
|
||||
# =================== /gestione_pickinglist.py ===================
|
||||
633
trash/layout_window.py.bak_fix_bc_transparent
Normal file
633
trash/layout_window.py.bak_fix_bc_transparent
Normal file
@@ -0,0 +1,633 @@
|
||||
from __future__ import annotations
|
||||
import tkinter as tk
|
||||
from tkinter import Menu, messagebox, filedialog
|
||||
import customtkinter as ctk
|
||||
from datetime import datetime
|
||||
|
||||
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||
|
||||
# ---- Color palette ----
|
||||
COLOR_EMPTY = "#B0B0B0" # grigio (vuota)
|
||||
COLOR_FULL = "#FFA500" # arancione (una UDC)
|
||||
COLOR_DOUBLE = "#D62728" # rosso (>=2 UDC)
|
||||
FG_DARK = "#111111"
|
||||
FG_LIGHT = "#FFFFFF"
|
||||
|
||||
|
||||
def pct_text(p_full: float, p_double: float | None = None) -> str:
|
||||
p_full = max(0.0, min(1.0, p_full))
|
||||
pf = round(p_full * 100, 1)
|
||||
pe = round(100 - pf, 1)
|
||||
if p_double and p_double > 0:
|
||||
pd = round(p_double * 100, 1)
|
||||
return f"Pieno {pf}% · Vuoto {pe}% (di cui doppie {pd}%)"
|
||||
return f"Pieno {pf}% · Vuoto {pe}%"
|
||||
|
||||
|
||||
class LayoutWindow(ctk.CTkToplevel):
|
||||
"""
|
||||
Visualizzazione layout corsie con matrice di celle.
|
||||
- Ogni cella è un pulsante colorato (vuota/piena/doppia)
|
||||
- Etichetta su DUE righe:
|
||||
1) "Corsia.Colonna.Fila" (una sola riga, senza andare a capo)
|
||||
2) barcode UDC (primo, se presente)
|
||||
- Ricerca per barcode UDC con cambio automatico corsia + highlight cella
|
||||
- Statistiche: globale e corsia selezionata
|
||||
- Export XLSX
|
||||
"""
|
||||
def __init__(self, parent: tk.Widget, db_app):
|
||||
super().__init__(parent)
|
||||
self.title("Warehouse · Layout corsie")
|
||||
self.geometry("1200x740")
|
||||
self.minsize(980, 560)
|
||||
self.resizable(True, True)
|
||||
|
||||
self.db = db_app
|
||||
self._busy = BusyOverlay(self)
|
||||
self._async = AsyncRunner(self)
|
||||
|
||||
# layout principale 5% / 80% / 15%
|
||||
self.grid_rowconfigure(0, weight=5)
|
||||
self.grid_rowconfigure(1, weight=80)
|
||||
self.grid_rowconfigure(2, weight=15)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# stato runtime
|
||||
self.corsia_selezionata = tk.StringVar()
|
||||
self.buttons: list[list[ctk.CTkButton]] = []
|
||||
self.btn_frames: list[list[ctk.CTkFrame]] = []
|
||||
self.matrix_state: list[list[int]] = [] # <— rinominata: prima era self.state
|
||||
self.fila_txt: list[list[str]] = []
|
||||
self.col_txt: list[list[str]] = []
|
||||
self.desc: list[list[str]] = []
|
||||
self.udc1: list[list[str]] = [] # primo barcode UDC trovato (o "")
|
||||
|
||||
# ricerca → focus differito (corsia, col, fila, barcode)
|
||||
self._pending_focus: tuple[str, str, str, str] | None = None
|
||||
self._highlighted: tuple[int, int] | None = None
|
||||
|
||||
# anti-race: token per ignorare risposte vecchie
|
||||
self._req_counter = 0
|
||||
self._last_req = 0
|
||||
|
||||
self._build_top()
|
||||
self._build_matrix_host()
|
||||
self._build_stats()
|
||||
|
||||
self._load_corsie()
|
||||
self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))
|
||||
|
||||
# ---------------- TOP BAR ----------------
|
||||
def _build_top(self):
|
||||
top = ctk.CTkFrame(self)
|
||||
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=6)
|
||||
for i in range(4):
|
||||
top.grid_columnconfigure(i, weight=0)
|
||||
top.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# lista corsie
|
||||
lf = ctk.CTkFrame(top)
|
||||
lf.grid(row=0, column=0, sticky="nsw")
|
||||
lf.grid_columnconfigure(0, weight=1)
|
||||
ctk.CTkLabel(lf, text="Corsie", font=("", 12, "bold")).grid(row=0, column=0, sticky="w", padx=6, pady=(6, 2))
|
||||
self.lb = tk.Listbox(lf, height=6, exportselection=False)
|
||||
self.lb.grid(row=1, column=0, sticky="nsw", padx=6, pady=(0, 6))
|
||||
self.lb.bind("<<ListboxSelect>>", self._on_select)
|
||||
|
||||
# search by barcode
|
||||
srch = ctk.CTkFrame(top)
|
||||
srch.grid(row=0, column=1, sticky="nsew", padx=(10, 10))
|
||||
self.search_var = tk.StringVar()
|
||||
self.search_entry = ctk.CTkEntry(srch, textvariable=self.search_var, width=260)
|
||||
self.search_entry.grid(row=0, column=0, sticky="w")
|
||||
ctk.CTkButton(srch, text="Cerca per barcode UDC", command=self._search_udc).grid(row=0, column=1, padx=(8, 0))
|
||||
srch.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# toolbar
|
||||
tb = ctk.CTkFrame(top)
|
||||
tb.grid(row=0, column=3, sticky="ne")
|
||||
ctk.CTkButton(tb, text="Aggiorna", command=self._refresh_current).grid(row=0, column=0, padx=4)
|
||||
ctk.CTkButton(tb, text="Export XLSX", command=self._export_xlsx).grid(row=0, column=1, padx=4)
|
||||
|
||||
# ---------------- MATRIX HOST ----------------
|
||||
def _build_matrix_host(self):
|
||||
center = ctk.CTkFrame(self)
|
||||
center.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 6))
|
||||
center.grid_rowconfigure(0, weight=1)
|
||||
center.grid_columnconfigure(0, weight=1)
|
||||
self.host = ctk.CTkFrame(center)
|
||||
self.host.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
|
||||
|
||||
def _apply_cell_style(self, btn: ctk.CTkButton, state: int):
|
||||
if state == 0:
|
||||
btn.configure(
|
||||
fg_color=COLOR_EMPTY, hover_color="#9A9A9A",
|
||||
text_color=FG_DARK, border_width=0, border_color="transparent"
|
||||
)
|
||||
elif state == 1:
|
||||
btn.configure(
|
||||
fg_color=COLOR_FULL, hover_color="#E69500",
|
||||
text_color=FG_DARK, border_width=0, border_color="transparent"
|
||||
)
|
||||
else:
|
||||
btn.configure(
|
||||
fg_color=COLOR_DOUBLE, hover_color="#B22222",
|
||||
text_color=FG_LIGHT, border_width=0, border_color="transparent"
|
||||
)
|
||||
|
||||
def _clear_highlight(self):
|
||||
if self._highlighted and self.buttons:
|
||||
r, c = self._highlighted
|
||||
try:
|
||||
if 0 <= r < len(self.buttons) and 0 <= c < len(self.buttons[r]):
|
||||
btn = self.buttons[r][c]
|
||||
if getattr(btn, "winfo_exists", None) and btn.winfo_exists():
|
||||
try:
|
||||
btn.configure(border_width=0, border_color="transparent")
|
||||
except Exception:
|
||||
pass
|
||||
# clear blue frame border
|
||||
try:
|
||||
fr = self.btn_frames[r][c]
|
||||
if fr and getattr(fr, "winfo_exists", None) and fr.winfo_exists():
|
||||
fr.configure(border_width=0, border_color="transparent")
|
||||
# in CTkFrame non esiste highlightthickness come in tk; border_* è corretto
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
self._highlighted = None
|
||||
|
||||
def _rebuild_matrix(self, rows: int, cols: int, state, fila_txt, col_txt, desc, udc1, corsia):
|
||||
# prima rimuovi highlight su vecchi bottoni
|
||||
self._clear_highlight()
|
||||
# ripulisci host
|
||||
for w in self.host.winfo_children():
|
||||
w.destroy()
|
||||
self.buttons.clear()
|
||||
self.btn_frames.clear()
|
||||
|
||||
# salva matrici
|
||||
self.matrix_state, self.fila_txt, self.col_txt, self.desc, self.udc1 = state, fila_txt, col_txt, desc, udc1
|
||||
|
||||
# ridistribuisci pesi griglia
|
||||
for r in range(rows):
|
||||
self.host.grid_rowconfigure(r, weight=1)
|
||||
for c in range(cols):
|
||||
self.host.grid_columnconfigure(c, weight=1)
|
||||
|
||||
# crea Frame+Button per cella (righe invertite: fila "a" in basso)
|
||||
for r in range(rows):
|
||||
row_btns = []
|
||||
row_frames = []
|
||||
for c in range(cols):
|
||||
st = state[r][c]
|
||||
code = f"{corsia}.{col_txt[r][c]}.{fila_txt[r][c]}" # PRIMA RIGA (in linea)
|
||||
udc = udc1[r][c] or "" # SECONDA RIGA: barcode UDC
|
||||
text = f"{code}\n{udc}"
|
||||
cell = ctk.CTkFrame(self.host, corner_radius=6, border_width=0)
|
||||
btn = ctk.CTkButton(
|
||||
cell,
|
||||
text=text,
|
||||
corner_radius=6,
|
||||
)
|
||||
self._apply_cell_style(btn, st)
|
||||
|
||||
rr = (rows - 1) - r # capovolgi
|
||||
cell.grid(row=rr, column=c, padx=1, pady=1, sticky="nsew")
|
||||
btn.pack(fill="both", expand=True)
|
||||
btn.configure(command=lambda rr=r, cc=c: self._open_menu(None, rr, cc))
|
||||
btn.bind("<Button-3>", lambda e, rr=r, cc=c: self._open_menu(e, rr, cc))
|
||||
row_btns.append(btn)
|
||||
row_frames.append(cell)
|
||||
self.buttons.append(row_btns)
|
||||
self.btn_frames.append(row_frames)
|
||||
|
||||
# focus differito post-ricarica
|
||||
if self._pending_focus and self._pending_focus[0] == corsia:
|
||||
_, col, fila, _barcode = self._pending_focus
|
||||
self._pending_focus = None
|
||||
self._highlight_cell_by_labels(col, fila)
|
||||
|
||||
# ---------------- CONTEXT MENU ----------------
|
||||
def _open_menu(self, event, r, c):
|
||||
st = self.matrix_state[r][c]
|
||||
corsia = self.corsia_selezionata.get()
|
||||
label = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}"
|
||||
m = Menu(self, tearoff=0)
|
||||
m.add_command(label="Apri dettaglio", command=lambda: self._toast(f"Dettaglio {label}"))
|
||||
if st == 0:
|
||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
||||
elif st == 1:
|
||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
||||
else:
|
||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
||||
m.add_separator()
|
||||
m.add_command(label="Copia ubicazione", command=lambda: self._copy(label))
|
||||
x = self.winfo_pointerx() if event is None else event.x_root
|
||||
y = self.winfo_pointery() if event is None else event.y_root
|
||||
m.tk_popup(x, y)
|
||||
|
||||
def _set_cell(self, r, c, val):
|
||||
self.matrix_state[r][c] = val
|
||||
btn = self.buttons[r][c]
|
||||
self._apply_cell_style(btn, val)
|
||||
self._refresh_stats()
|
||||
|
||||
# ---------------- STATS ----------------
|
||||
def _build_stats(self):
|
||||
bottom = ctk.CTkFrame(self)
|
||||
bottom.grid(row=2, column=0, sticky="nsew", padx=8, pady=6)
|
||||
bottom.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkLabel(bottom, text="Riempimento globale", font=("", 10, "bold")).grid(row=0, column=0, sticky="w", pady=(0, 2))
|
||||
self.tot_canvas = tk.Canvas(bottom, height=22, highlightthickness=0)
|
||||
self.tot_canvas.grid(row=1, column=0, sticky="ew", padx=(0, 260))
|
||||
self.tot_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0))
|
||||
self.tot_text.grid(row=1, column=0, sticky="e")
|
||||
|
||||
ctk.CTkLabel(bottom, text="Riempimento corsia selezionata", font=("", 10, "bold")).grid(row=2, column=0, sticky="w", pady=(10, 2))
|
||||
self.sel_canvas = tk.Canvas(bottom, height=22, highlightthickness=0)
|
||||
self.sel_canvas.grid(row=3, column=0, sticky="ew", padx=(0, 260))
|
||||
self.sel_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0))
|
||||
self.sel_text.grid(row=3, column=0, sticky="e")
|
||||
|
||||
leg = ctk.CTkFrame(bottom)
|
||||
leg.grid(row=4, column=0, sticky="w", pady=(10, 0))
|
||||
ctk.CTkLabel(leg, text="Legenda celle:").grid(row=0, column=0, padx=(0, 8))
|
||||
self._legend(leg, 1, "Vuota", COLOR_EMPTY)
|
||||
self._legend(leg, 3, "Piena", COLOR_FULL)
|
||||
self._legend(leg, 5, "Doppia UDC", COLOR_DOUBLE)
|
||||
|
||||
def _legend(self, parent, col, text, color):
|
||||
box = tk.Canvas(parent, width=18, height=12, highlightthickness=0)
|
||||
box.create_rectangle(0, 0, 18, 12, fill=color, width=1, outline="#444")
|
||||
box.grid(row=0, column=col)
|
||||
ctk.CTkLabel(parent, text=text).grid(row=0, column=col + 1, padx=(4, 12))
|
||||
|
||||
# ---------------- DATA LOADING ----------------
|
||||
def _load_corsie(self):
|
||||
sql = """
|
||||
WITH C AS (
|
||||
SELECT DISTINCT LTRIM(RTRIM(Corsia)) AS Corsia
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL) AND LTRIM(RTRIM(Corsia)) <> '7G'
|
||||
)
|
||||
SELECT Corsia
|
||||
FROM C
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LEFT(Corsia,3)='MAG' AND TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) IS NOT NULL THEN 0
|
||||
WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
CASE WHEN LEFT(Corsia,3)='MAG' THEN TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) END,
|
||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN TRY_CONVERT(int, Corsia) END,
|
||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN SUBSTRING(Corsia, LEN(CAST(TRY_CONVERT(int, Corsia) AS varchar(20)))+1, 50) END,
|
||||
Corsia;
|
||||
"""
|
||||
def _ok(res):
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
self.lb.delete(0, tk.END)
|
||||
corsie = [r[0] for r in rows]
|
||||
for c in corsie:
|
||||
self.lb.insert(tk.END, c)
|
||||
idx = corsie.index("1A") if "1A" in corsie else (0 if corsie else -1)
|
||||
if idx >= 0:
|
||||
self.lb.selection_clear(0, tk.END)
|
||||
self.lb.selection_set(idx)
|
||||
self.lb.see(idx)
|
||||
self._on_select(None)
|
||||
else:
|
||||
self._toast("Nessuna corsia trovata.")
|
||||
self._busy.hide()
|
||||
def _err(ex):
|
||||
self._busy.hide()
|
||||
messagebox.showerror("Errore", f"Caricamento corsie fallito:\n{ex}")
|
||||
self._async.run(self.db.query_json(sql, {}), _ok, _err, busy=self._busy, message="Carico corsie…")
|
||||
|
||||
def _on_select(self, _):
|
||||
sel = self.lb.curselection()
|
||||
if not sel:
|
||||
return
|
||||
corsia = self.lb.get(sel[0])
|
||||
self.corsia_selezionata.set(corsia)
|
||||
self._load_matrix(corsia)
|
||||
|
||||
def _select_corsia_in_listbox(self, corsia: str):
|
||||
for i in range(self.lb.size()):
|
||||
if self.lb.get(i) == corsia:
|
||||
self.lb.selection_clear(0, tk.END)
|
||||
self.lb.selection_set(i)
|
||||
self.lb.see(i)
|
||||
break
|
||||
|
||||
def _load_matrix(self, corsia: str):
|
||||
# nuovo token richiesta → evita che risposte vecchie spazzino la UI
|
||||
self._req_counter += 1
|
||||
req_id = self._req_counter
|
||||
self._last_req = req_id
|
||||
|
||||
sql = """
|
||||
WITH C AS (
|
||||
SELECT
|
||||
ID,
|
||||
LTRIM(RTRIM(Corsia)) AS Corsia,
|
||||
LTRIM(RTRIM(Fila)) AS Fila,
|
||||
LTRIM(RTRIM(Colonna)) AS Colonna,
|
||||
Descrizione
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||
AND LTRIM(RTRIM(Corsia)) <> '7G' AND LTRIM(RTRIM(Corsia)) = :corsia
|
||||
),
|
||||
R AS (
|
||||
SELECT Fila,
|
||||
DENSE_RANK() OVER (
|
||||
ORDER BY CASE WHEN TRY_CONVERT(int, Fila) IS NULL THEN 1 ELSE 0 END,
|
||||
TRY_CONVERT(int, Fila), Fila
|
||||
) AS RowN
|
||||
FROM C GROUP BY Fila
|
||||
),
|
||||
K AS (
|
||||
SELECT Colonna,
|
||||
DENSE_RANK() OVER (
|
||||
ORDER BY CASE WHEN TRY_CONVERT(int, Colonna) IS NULL THEN 1 ELSE 0 END,
|
||||
TRY_CONVERT(int, Colonna), Colonna
|
||||
) AS ColN
|
||||
FROM C GROUP BY Colonna
|
||||
),
|
||||
S AS (
|
||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
||||
FROM C AS c
|
||||
LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
),
|
||||
U AS (
|
||||
SELECT c.ID, MIN(g.BarcodePallet) AS FirstUDC
|
||||
FROM C c
|
||||
LEFT JOIN dbo.XMag_GiacenzaPallet g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
)
|
||||
SELECT
|
||||
r.RowN, k.ColN,
|
||||
CASE WHEN s.n IS NULL OR s.n = 0 THEN 0
|
||||
WHEN s.n = 1 THEN 1
|
||||
ELSE 2 END AS Stato,
|
||||
c.Descrizione,
|
||||
LTRIM(RTRIM(c.Fila)) AS FilaTxt,
|
||||
LTRIM(RTRIM(c.Colonna)) AS ColTxt,
|
||||
U.FirstUDC
|
||||
FROM C c
|
||||
JOIN R r ON r.Fila = c.Fila
|
||||
JOIN K k ON k.Colonna = c.Colonna
|
||||
LEFT JOIN S s ON s.ID = c.ID
|
||||
LEFT JOIN U ON U.ID = c.ID
|
||||
ORDER BY r.RowN, k.ColN;
|
||||
"""
|
||||
def _ok(res):
|
||||
# ignora risposte superate
|
||||
if req_id < self._last_req:
|
||||
return
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
if not rows:
|
||||
# mostra matrice vuota senza rimuovere il frame (evita "schermo bianco")
|
||||
self._rebuild_matrix(0, 0, [], [], [], [], [], corsia)
|
||||
self._refresh_stats()
|
||||
self._busy.hide()
|
||||
return
|
||||
max_r = max_c = 0
|
||||
for row in rows:
|
||||
rown, coln = row[0], row[1]
|
||||
if rown and coln:
|
||||
max_r = max(max_r, int(rown))
|
||||
max_c = max(max_c, int(coln))
|
||||
mat = [[0] * max_c for _ in range(max_r)]
|
||||
fila = [[""] * max_c for _ in range(max_r)]
|
||||
col = [[""] * max_c for _ in range(max_r)]
|
||||
desc = [[""] * max_c for _ in range(max_r)]
|
||||
udc = [[""] * max_c for _ in range(max_r)]
|
||||
for row in rows:
|
||||
rown, coln, stato, descr, fila_txt, col_txt, first_udc = row
|
||||
r = int(rown) - 1
|
||||
c = int(coln) - 1
|
||||
mat[r][c] = int(stato)
|
||||
fila[r][c] = str(fila_txt or "")
|
||||
col[r][c] = str(col_txt or "")
|
||||
desc[r][c] = str(descr or f"{corsia}.{col_txt}.{fila_txt}")
|
||||
udc[r][c] = str(first_udc or "")
|
||||
self._rebuild_matrix(max_r, max_c, mat, fila, col, desc, udc, corsia)
|
||||
self._refresh_stats()
|
||||
self._busy.hide()
|
||||
def _err(ex):
|
||||
if req_id < self._last_req:
|
||||
return
|
||||
self._busy.hide()
|
||||
messagebox.showerror("Errore", f"Caricamento matrice {corsia} fallito:\n{ex}")
|
||||
self._async.run(self.db.query_json(sql, {"corsia": corsia}), _ok, _err, busy=self._busy, message=f"Carico corsia {corsia}…")
|
||||
|
||||
# ---------------- SEARCH ----------------
|
||||
def _search_udc(self):
|
||||
barcode = (self.search_var.get() or "").strip()
|
||||
if not barcode:
|
||||
self._toast("Inserisci un barcode UDC da cercare.")
|
||||
return
|
||||
|
||||
# bump token per impedire che una vecchia _load_matrix cancelli UI
|
||||
self._req_counter += 1
|
||||
search_req_id = self._req_counter
|
||||
self._last_req = search_req_id
|
||||
|
||||
sql = """
|
||||
SELECT TOP (1)
|
||||
RTRIM(c.Corsia) AS Corsia,
|
||||
RTRIM(c.Colonna) AS Colonna,
|
||||
RTRIM(c.Fila) AS Fila,
|
||||
c.ID AS IDCella
|
||||
FROM dbo.XMag_GiacenzaPallet g
|
||||
JOIN dbo.Celle c ON c.ID = g.IDCella
|
||||
WHERE g.BarcodePallet = :barcode
|
||||
AND c.ID <> 9999 AND RTRIM(c.Corsia) <> '7G'
|
||||
"""
|
||||
def _ok(res):
|
||||
if search_req_id < self._last_req:
|
||||
return
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
if not rows:
|
||||
messagebox.showinfo("Ricerca", f"UDC {barcode} non trovata.", parent=self)
|
||||
return
|
||||
corsia, col, fila, _idc = rows[0]
|
||||
corsia = str(corsia).strip(); col = str(col).strip(); fila = str(fila).strip()
|
||||
self._pending_focus = (corsia, col, fila, barcode)
|
||||
|
||||
# sincronizza listbox e carica SEMPRE la corsia della UDC
|
||||
self._select_corsia_in_listbox(corsia)
|
||||
self.corsia_selezionata.set(corsia)
|
||||
self._load_matrix(corsia) # highlight avverrà in _rebuild_matrix
|
||||
def _err(ex):
|
||||
if search_req_id < self._last_req:
|
||||
return
|
||||
messagebox.showerror("Ricerca", f"Errore ricerca UDC:\n{ex}", parent=self)
|
||||
|
||||
self._async.run(self.db.query_json(sql, {"barcode": barcode}), _ok, _err, busy=self._busy, message="Cerco UDC…")
|
||||
|
||||
def _try_highlight(self, col_txt: str, fila_txt: str) -> bool:
|
||||
for r in range(len(self.col_txt)):
|
||||
for c in range(len(self.col_txt[r])):
|
||||
if self.col_txt[r][c] == col_txt and self.fila_txt[r][c] == fila_txt:
|
||||
self._clear_highlight()
|
||||
btn = self.buttons[r][c]
|
||||
btn.configure(border_width=3, border_color="blue")
|
||||
try:
|
||||
fr = self.btn_frames[r][c]
|
||||
fr.configure(border_color="blue", border_width=2)
|
||||
except Exception:
|
||||
pass
|
||||
self._highlighted = (r, c)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _highlight_cell_by_labels(self, col_txt: str, fila_txt: str):
|
||||
if not self._try_highlight(col_txt, fila_txt):
|
||||
self._toast("Cella trovata ma non mappabile a pulsante.")
|
||||
|
||||
# ---------------- COMMANDS ----------------
|
||||
def _refresh_current(self):
|
||||
if self.corsia_selezionata.get():
|
||||
self._load_matrix(self.corsia_selezionata.get())
|
||||
|
||||
def _export_xlsx(self):
|
||||
if not self.matrix_state:
|
||||
messagebox.showinfo("Export", "Nessuna matrice da esportare.")
|
||||
return
|
||||
corsia = self.corsia_selezionata.get() or "NA"
|
||||
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
|
||||
default = f"layout_matrice_{corsia}_{ts}.xlsx"
|
||||
path = filedialog.asksaveasfilename(
|
||||
title="Esporta matrice",
|
||||
defaultextension=".xlsx",
|
||||
initialfile=default,
|
||||
filetypes=[("Excel", "*.xlsx")]
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import PatternFill, Alignment, Font
|
||||
except Exception as ex:
|
||||
messagebox.showerror("Export", f"Manca openpyxl: {ex}\nInstalla con: pip install openpyxl")
|
||||
return
|
||||
rows = len(self.matrix_state)
|
||||
cols = len(self.matrix_state[0]) if self.matrix_state else 0
|
||||
wb = Workbook()
|
||||
ws1 = wb.active
|
||||
ws1.title = f"Dettaglio {corsia}"
|
||||
ws1.append(["Corsia", "FilaIdx", "ColIdx", "Stato", "Descrizione", "FilaTxt", "ColTxt", "UDC1"])
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
st = self.matrix_state[r][c]
|
||||
stato_lbl = "Vuota" if st == 0 else ("Piena" if st == 1 else "Doppia")
|
||||
ws1.append([corsia, r + 1, c + 1, stato_lbl,
|
||||
self.desc[r][c], self.fila_txt[r][c], self.col_txt[r][c], self.udc1[r][c]])
|
||||
for cell in ws1[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
ws2 = wb.create_sheet(f"Matrice {corsia}")
|
||||
fills = {
|
||||
0: PatternFill("solid", fgColor="B0B0B0"),
|
||||
1: PatternFill("solid", fgColor="FFA500"),
|
||||
2: PatternFill("solid", fgColor="D62728"),
|
||||
}
|
||||
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
value = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}\n{self.udc1[r][c]}"
|
||||
cell = ws2.cell(row=(rows - r), column=c + 1, value=value) # capovolto per avere 'a' in basso
|
||||
cell.fill = fills.get(self.matrix_state[r][c], fills[0])
|
||||
cell.alignment = center
|
||||
try:
|
||||
wb.save(path)
|
||||
self._toast(f"Esportato: {path}")
|
||||
except Exception as ex:
|
||||
messagebox.showerror("Export", f"Salvataggio fallito:\n{ex}")
|
||||
|
||||
# ---------------- STATS ----------------
|
||||
def _refresh_stats(self):
|
||||
# globale dal DB
|
||||
sql_tot = """
|
||||
WITH C AS (
|
||||
SELECT ID
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||
AND LTRIM(RTRIM(Corsia)) <> '7G'
|
||||
AND LTRIM(RTRIM(Fila)) IS NOT NULL
|
||||
AND LTRIM(RTRIM(Colonna)) IS NOT NULL
|
||||
),
|
||||
S AS (
|
||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
||||
FROM C AS c LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
)
|
||||
SELECT
|
||||
CAST(SUM(CASE WHEN s.n>0 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercPieno,
|
||||
CAST(SUM(CASE WHEN s.n>1 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercDoppie
|
||||
FROM C LEFT JOIN S s ON s.ID = C.ID;
|
||||
"""
|
||||
def _ok(res):
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
p_full = float(rows[0][0] or 0.0) if rows else 0.0
|
||||
p_dbl = float(rows[0][1] or 0.0) if rows else 0.0
|
||||
self._draw_bar(self.tot_canvas, p_full)
|
||||
self.tot_text.configure(text=pct_text(p_full, p_dbl))
|
||||
self._async.run(self.db.query_json(sql_tot, {}), _ok, lambda e: None, busy=None, message=None)
|
||||
|
||||
# selezionata dalla matrice in memoria
|
||||
if self.matrix_state:
|
||||
tot = sum(len(r) for r in self.matrix_state)
|
||||
full = sum(1 for row in self.matrix_state for v in row if v in (1, 2))
|
||||
doubles = sum(1 for row in self.matrix_state for v in row if v == 2)
|
||||
p_full = (full / tot) if tot else 0.0
|
||||
p_dbl = (doubles / tot) if tot else 0.0
|
||||
else:
|
||||
p_full = p_dbl = 0.0
|
||||
self._draw_bar(self.sel_canvas, p_full)
|
||||
self.sel_text.configure(text=pct_text(p_full, p_dbl))
|
||||
|
||||
def _draw_bar(self, cv: tk.Canvas, p_full: float):
|
||||
cv.delete("all")
|
||||
w = max(300, cv.winfo_width() or 600)
|
||||
h = 18
|
||||
fw = int(w * max(0.0, min(1.0, p_full)))
|
||||
cv.create_rectangle(2, 2, 2 + fw, 2 + h, fill="#D62728", width=0)
|
||||
cv.create_rectangle(2 + fw, 2, 2 + w, 2 + h, fill="#2CA02C", width=0)
|
||||
cv.create_rectangle(2, 2, 2 + w, 2 + h, outline="#555", width=1)
|
||||
|
||||
# ---------------- UTIL ----------------
|
||||
def _toast(self, msg, ms=1400):
|
||||
if not hasattr(self, "_status"):
|
||||
self._status = ctk.CTkLabel(self, anchor="w")
|
||||
self._status.grid(row=3, column=0, sticky="ew")
|
||||
self._status.configure(text=msg)
|
||||
self.after(ms, lambda: self._status.configure(text=""))
|
||||
|
||||
def _copy(self, txt: str):
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(txt)
|
||||
self._toast(f"Copiato: {txt}")
|
||||
|
||||
|
||||
def open_layout_window(parent, db_app):
|
||||
key = "_layout_window_singleton"
|
||||
ex = getattr(parent, key, None)
|
||||
if ex and ex.winfo_exists():
|
||||
try:
|
||||
ex.lift()
|
||||
ex.focus_force()
|
||||
return ex
|
||||
except Exception:
|
||||
pass
|
||||
w = LayoutWindow(parent, db_app)
|
||||
setattr(parent, key, w)
|
||||
return w
|
||||
632
trash/layout_window.py.bak_perf
Normal file
632
trash/layout_window.py.bak_perf
Normal file
@@ -0,0 +1,632 @@
|
||||
from __future__ import annotations
|
||||
import tkinter as tk
|
||||
from tkinter import Menu, messagebox, filedialog
|
||||
import customtkinter as ctk
|
||||
from datetime import datetime
|
||||
|
||||
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||
|
||||
# ---- Color palette ----
|
||||
COLOR_EMPTY = "#B0B0B0" # grigio (vuota)
|
||||
COLOR_FULL = "#FFA500" # arancione (una UDC)
|
||||
COLOR_DOUBLE = "#D62728" # rosso (>=2 UDC)
|
||||
FG_DARK = "#111111"
|
||||
FG_LIGHT = "#FFFFFF"
|
||||
|
||||
|
||||
def pct_text(p_full: float, p_double: float | None = None) -> str:
|
||||
p_full = max(0.0, min(1.0, p_full))
|
||||
pf = round(p_full * 100, 1)
|
||||
pe = round(100 - pf, 1)
|
||||
if p_double and p_double > 0:
|
||||
pd = round(p_double * 100, 1)
|
||||
return f"Pieno {pf}% · Vuoto {pe}% (di cui doppie {pd}%)"
|
||||
return f"Pieno {pf}% · Vuoto {pe}%"
|
||||
|
||||
|
||||
class LayoutWindow(ctk.CTkToplevel):
|
||||
"""
|
||||
Visualizzazione layout corsie con matrice di celle.
|
||||
- Ogni cella è un pulsante colorato (vuota/piena/doppia)
|
||||
- Etichetta su DUE righe:
|
||||
1) "Corsia.Colonna.Fila" (una sola riga, senza andare a capo)
|
||||
2) barcode UDC (primo, se presente)
|
||||
- Ricerca per barcode UDC con cambio automatico corsia + highlight cella
|
||||
- Statistiche: globale e corsia selezionata
|
||||
- Export XLSX
|
||||
"""
|
||||
def __init__(self, parent: tk.Widget, db_app):
|
||||
super().__init__(parent)
|
||||
self.title("Warehouse · Layout corsie")
|
||||
self.geometry("1200x740")
|
||||
self.minsize(980, 560)
|
||||
self.resizable(True, True)
|
||||
|
||||
self.db = db_app
|
||||
self._busy = BusyOverlay(self)
|
||||
self._async = AsyncRunner(self)
|
||||
|
||||
# layout principale 5% / 80% / 15%
|
||||
self.grid_rowconfigure(0, weight=5)
|
||||
self.grid_rowconfigure(1, weight=80)
|
||||
self.grid_rowconfigure(2, weight=15)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# stato runtime
|
||||
self.corsia_selezionata = tk.StringVar()
|
||||
self.buttons: list[list[ctk.CTkButton]] = []
|
||||
self.btn_frames: list[list[ctk.CTkFrame]] = []
|
||||
self.matrix_state: list[list[int]] = [] # <— rinominata: prima era self.state
|
||||
self.fila_txt: list[list[str]] = []
|
||||
self.col_txt: list[list[str]] = []
|
||||
self.desc: list[list[str]] = []
|
||||
self.udc1: list[list[str]] = [] # primo barcode UDC trovato (o "")
|
||||
|
||||
# ricerca → focus differito (corsia, col, fila, barcode)
|
||||
self._pending_focus: tuple[str, str, str, str] | None = None
|
||||
self._highlighted: tuple[int, int] | None = None
|
||||
|
||||
# anti-race: token per ignorare risposte vecchie
|
||||
self._req_counter = 0
|
||||
self._last_req = 0
|
||||
|
||||
self._build_top()
|
||||
self._build_matrix_host()
|
||||
self._build_stats()
|
||||
|
||||
self._load_corsie()
|
||||
self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))
|
||||
|
||||
# ---------------- TOP BAR ----------------
|
||||
def _build_top(self):
|
||||
top = ctk.CTkFrame(self)
|
||||
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=6)
|
||||
for i in range(4):
|
||||
top.grid_columnconfigure(i, weight=0)
|
||||
top.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# lista corsie
|
||||
lf = ctk.CTkFrame(top)
|
||||
lf.grid(row=0, column=0, sticky="nsw")
|
||||
lf.grid_columnconfigure(0, weight=1)
|
||||
ctk.CTkLabel(lf, text="Corsie", font=("", 12, "bold")).grid(row=0, column=0, sticky="w", padx=6, pady=(6, 2))
|
||||
self.lb = tk.Listbox(lf, height=6, exportselection=False)
|
||||
self.lb.grid(row=1, column=0, sticky="nsw", padx=6, pady=(0, 6))
|
||||
self.lb.bind("<<ListboxSelect>>", self._on_select)
|
||||
|
||||
# search by barcode
|
||||
srch = ctk.CTkFrame(top)
|
||||
srch.grid(row=0, column=1, sticky="nsew", padx=(10, 10))
|
||||
self.search_var = tk.StringVar()
|
||||
self.search_entry = ctk.CTkEntry(srch, textvariable=self.search_var, width=260)
|
||||
self.search_entry.grid(row=0, column=0, sticky="w")
|
||||
ctk.CTkButton(srch, text="Cerca per barcode UDC", command=self._search_udc).grid(row=0, column=1, padx=(8, 0))
|
||||
srch.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# toolbar
|
||||
tb = ctk.CTkFrame(top)
|
||||
tb.grid(row=0, column=3, sticky="ne")
|
||||
ctk.CTkButton(tb, text="Aggiorna", command=self._refresh_current).grid(row=0, column=0, padx=4)
|
||||
ctk.CTkButton(tb, text="Export XLSX", command=self._export_xlsx).grid(row=0, column=1, padx=4)
|
||||
|
||||
# ---------------- MATRIX HOST ----------------
|
||||
def _build_matrix_host(self):
|
||||
center = ctk.CTkFrame(self)
|
||||
center.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 6))
|
||||
center.grid_rowconfigure(0, weight=1)
|
||||
center.grid_columnconfigure(0, weight=1)
|
||||
self.host = ctk.CTkFrame(center)
|
||||
self.host.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
|
||||
|
||||
def _apply_cell_style(self, btn: ctk.CTkButton, state: int):
|
||||
if state == 0:
|
||||
btn.configure(
|
||||
fg_color=COLOR_EMPTY, hover_color="#9A9A9A",
|
||||
text_color=FG_DARK, border_width=0
|
||||
)
|
||||
elif state == 1:
|
||||
btn.configure(
|
||||
fg_color=COLOR_FULL, hover_color="#E69500",
|
||||
text_color=FG_DARK, border_width=0
|
||||
)
|
||||
else:
|
||||
btn.configure(
|
||||
fg_color=COLOR_DOUBLE, hover_color="#B22222",
|
||||
text_color=FG_LIGHT, border_width=0
|
||||
)
|
||||
|
||||
def _clear_highlight(self):
|
||||
if self._highlighted and self.buttons:
|
||||
r, c = self._highlighted
|
||||
try:
|
||||
if 0 <= r < len(self.buttons) and 0 <= c < len(self.buttons[r]):
|
||||
btn = self.buttons[r][c]
|
||||
if getattr(btn, "winfo_exists", None) and btn.winfo_exists():
|
||||
try:
|
||||
btn.configure(border_width=0)
|
||||
except Exception:
|
||||
pass
|
||||
# clear blue frame border
|
||||
try:
|
||||
fr = self.btn_frames[r][c]
|
||||
if fr and getattr(fr, "winfo_exists", None) and fr.winfo_exists():
|
||||
fr.configure(border_width=0)
|
||||
# in CTkFrame non esiste highlightthickness come in tk; border_* è corretto
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
self._highlighted = None
|
||||
|
||||
def _rebuild_matrix(self, rows: int, cols: int, state, fila_txt, col_txt, desc, udc1, corsia):
|
||||
# prima rimuovi highlight su vecchi bottoni
|
||||
self._clear_highlight()
|
||||
# ripulisci host
|
||||
for w in self.host.winfo_children():
|
||||
w.destroy()
|
||||
self.buttons.clear()
|
||||
self.btn_frames.clear()
|
||||
|
||||
# salva matrici
|
||||
self.matrix_state, self.fila_txt, self.col_txt, self.desc, self.udc1 = state, fila_txt, col_txt, desc, udc1
|
||||
|
||||
# ridistribuisci pesi griglia
|
||||
for r in range(rows):
|
||||
self.host.grid_rowconfigure(r, weight=1)
|
||||
for c in range(cols):
|
||||
self.host.grid_columnconfigure(c, weight=1)
|
||||
|
||||
# crea Frame+Button per cella (righe invertite: fila "a" in basso)
|
||||
for r in range(rows):
|
||||
row_btns = []
|
||||
row_frames = []
|
||||
for c in range(cols):
|
||||
st = state[r][c]
|
||||
code = f"{corsia}.{col_txt[r][c]}.{fila_txt[r][c]}" # PRIMA RIGA (in linea)
|
||||
udc = udc1[r][c] or "" # SECONDA RIGA: barcode UDC
|
||||
text = f"{code}\n{udc}"
|
||||
cell = ctk.CTkFrame(self.host, corner_radius=6, border_width=0)
|
||||
btn = ctk.CTkButton(
|
||||
cell,
|
||||
text=text,
|
||||
corner_radius=6)
|
||||
self._apply_cell_style(btn, st)
|
||||
|
||||
rr = (rows - 1) - r # capovolgi
|
||||
cell.grid(row=rr, column=c, padx=1, pady=1, sticky="nsew")
|
||||
btn.pack(fill="both", expand=True)
|
||||
btn.configure(command=lambda rr=r, cc=c: self._open_menu(None, rr, cc))
|
||||
btn.bind("<Button-3>", lambda e, rr=r, cc=c: self._open_menu(e, rr, cc))
|
||||
row_btns.append(btn)
|
||||
row_frames.append(cell)
|
||||
self.buttons.append(row_btns)
|
||||
self.btn_frames.append(row_frames)
|
||||
|
||||
# focus differito post-ricarica
|
||||
if self._pending_focus and self._pending_focus[0] == corsia:
|
||||
_, col, fila, _barcode = self._pending_focus
|
||||
self._pending_focus = None
|
||||
self._highlight_cell_by_labels(col, fila)
|
||||
|
||||
# ---------------- CONTEXT MENU ----------------
|
||||
def _open_menu(self, event, r, c):
|
||||
st = self.matrix_state[r][c]
|
||||
corsia = self.corsia_selezionata.get()
|
||||
label = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}"
|
||||
m = Menu(self, tearoff=0)
|
||||
m.add_command(label="Apri dettaglio", command=lambda: self._toast(f"Dettaglio {label}"))
|
||||
if st == 0:
|
||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
||||
elif st == 1:
|
||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
||||
else:
|
||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
||||
m.add_separator()
|
||||
m.add_command(label="Copia ubicazione", command=lambda: self._copy(label))
|
||||
x = self.winfo_pointerx() if event is None else event.x_root
|
||||
y = self.winfo_pointery() if event is None else event.y_root
|
||||
m.tk_popup(x, y)
|
||||
|
||||
def _set_cell(self, r, c, val):
|
||||
self.matrix_state[r][c] = val
|
||||
btn = self.buttons[r][c]
|
||||
self._apply_cell_style(btn, val)
|
||||
self._refresh_stats()
|
||||
|
||||
# ---------------- STATS ----------------
|
||||
def _build_stats(self):
|
||||
bottom = ctk.CTkFrame(self)
|
||||
bottom.grid(row=2, column=0, sticky="nsew", padx=8, pady=6)
|
||||
bottom.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkLabel(bottom, text="Riempimento globale", font=("", 10, "bold")).grid(row=0, column=0, sticky="w", pady=(0, 2))
|
||||
self.tot_canvas = tk.Canvas(bottom, height=22, highlightthickness=0)
|
||||
self.tot_canvas.grid(row=1, column=0, sticky="ew", padx=(0, 260))
|
||||
self.tot_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0))
|
||||
self.tot_text.grid(row=1, column=0, sticky="e")
|
||||
|
||||
ctk.CTkLabel(bottom, text="Riempimento corsia selezionata", font=("", 10, "bold")).grid(row=2, column=0, sticky="w", pady=(10, 2))
|
||||
self.sel_canvas = tk.Canvas(bottom, height=22, highlightthickness=0)
|
||||
self.sel_canvas.grid(row=3, column=0, sticky="ew", padx=(0, 260))
|
||||
self.sel_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0))
|
||||
self.sel_text.grid(row=3, column=0, sticky="e")
|
||||
|
||||
leg = ctk.CTkFrame(bottom)
|
||||
leg.grid(row=4, column=0, sticky="w", pady=(10, 0))
|
||||
ctk.CTkLabel(leg, text="Legenda celle:").grid(row=0, column=0, padx=(0, 8))
|
||||
self._legend(leg, 1, "Vuota", COLOR_EMPTY)
|
||||
self._legend(leg, 3, "Piena", COLOR_FULL)
|
||||
self._legend(leg, 5, "Doppia UDC", COLOR_DOUBLE)
|
||||
|
||||
def _legend(self, parent, col, text, color):
|
||||
box = tk.Canvas(parent, width=18, height=12, highlightthickness=0)
|
||||
box.create_rectangle(0, 0, 18, 12, fill=color, width=1, outline="#444")
|
||||
box.grid(row=0, column=col)
|
||||
ctk.CTkLabel(parent, text=text).grid(row=0, column=col + 1, padx=(4, 12))
|
||||
|
||||
# ---------------- DATA LOADING ----------------
|
||||
def _load_corsie(self):
|
||||
sql = """
|
||||
WITH C AS (
|
||||
SELECT DISTINCT LTRIM(RTRIM(Corsia)) AS Corsia
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL) AND LTRIM(RTRIM(Corsia)) <> '7G'
|
||||
)
|
||||
SELECT Corsia
|
||||
FROM C
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LEFT(Corsia,3)='MAG' AND TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) IS NOT NULL THEN 0
|
||||
WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
CASE WHEN LEFT(Corsia,3)='MAG' THEN TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) END,
|
||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN TRY_CONVERT(int, Corsia) END,
|
||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN SUBSTRING(Corsia, LEN(CAST(TRY_CONVERT(int, Corsia) AS varchar(20)))+1, 50) END,
|
||||
Corsia;
|
||||
"""
|
||||
def _ok(res):
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
self.lb.delete(0, tk.END)
|
||||
corsie = [r[0] for r in rows]
|
||||
for c in corsie:
|
||||
self.lb.insert(tk.END, c)
|
||||
idx = corsie.index("1A") if "1A" in corsie else (0 if corsie else -1)
|
||||
if idx >= 0:
|
||||
self.lb.selection_clear(0, tk.END)
|
||||
self.lb.selection_set(idx)
|
||||
self.lb.see(idx)
|
||||
self._on_select(None)
|
||||
else:
|
||||
self._toast("Nessuna corsia trovata.")
|
||||
self._busy.hide()
|
||||
def _err(ex):
|
||||
self._busy.hide()
|
||||
messagebox.showerror("Errore", f"Caricamento corsie fallito:\n{ex}")
|
||||
self._async.run(self.db.query_json(sql, {}), _ok, _err, busy=self._busy, message="Carico corsie…")
|
||||
|
||||
def _on_select(self, _):
|
||||
sel = self.lb.curselection()
|
||||
if not sel:
|
||||
return
|
||||
corsia = self.lb.get(sel[0])
|
||||
self.corsia_selezionata.set(corsia)
|
||||
self._load_matrix(corsia)
|
||||
|
||||
def _select_corsia_in_listbox(self, corsia: str):
|
||||
for i in range(self.lb.size()):
|
||||
if self.lb.get(i) == corsia:
|
||||
self.lb.selection_clear(0, tk.END)
|
||||
self.lb.selection_set(i)
|
||||
self.lb.see(i)
|
||||
break
|
||||
|
||||
def _load_matrix(self, corsia: str):
|
||||
# nuovo token richiesta → evita che risposte vecchie spazzino la UI
|
||||
self._req_counter += 1
|
||||
req_id = self._req_counter
|
||||
self._last_req = req_id
|
||||
|
||||
sql = """
|
||||
WITH C AS (
|
||||
SELECT
|
||||
ID,
|
||||
LTRIM(RTRIM(Corsia)) AS Corsia,
|
||||
LTRIM(RTRIM(Fila)) AS Fila,
|
||||
LTRIM(RTRIM(Colonna)) AS Colonna,
|
||||
Descrizione
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||
AND LTRIM(RTRIM(Corsia)) <> '7G' AND LTRIM(RTRIM(Corsia)) = :corsia
|
||||
),
|
||||
R AS (
|
||||
SELECT Fila,
|
||||
DENSE_RANK() OVER (
|
||||
ORDER BY CASE WHEN TRY_CONVERT(int, Fila) IS NULL THEN 1 ELSE 0 END,
|
||||
TRY_CONVERT(int, Fila), Fila
|
||||
) AS RowN
|
||||
FROM C GROUP BY Fila
|
||||
),
|
||||
K AS (
|
||||
SELECT Colonna,
|
||||
DENSE_RANK() OVER (
|
||||
ORDER BY CASE WHEN TRY_CONVERT(int, Colonna) IS NULL THEN 1 ELSE 0 END,
|
||||
TRY_CONVERT(int, Colonna), Colonna
|
||||
) AS ColN
|
||||
FROM C GROUP BY Colonna
|
||||
),
|
||||
S AS (
|
||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
||||
FROM C AS c
|
||||
LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
),
|
||||
U AS (
|
||||
SELECT c.ID, MIN(g.BarcodePallet) AS FirstUDC
|
||||
FROM C c
|
||||
LEFT JOIN dbo.XMag_GiacenzaPallet g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
)
|
||||
SELECT
|
||||
r.RowN, k.ColN,
|
||||
CASE WHEN s.n IS NULL OR s.n = 0 THEN 0
|
||||
WHEN s.n = 1 THEN 1
|
||||
ELSE 2 END AS Stato,
|
||||
c.Descrizione,
|
||||
LTRIM(RTRIM(c.Fila)) AS FilaTxt,
|
||||
LTRIM(RTRIM(c.Colonna)) AS ColTxt,
|
||||
U.FirstUDC
|
||||
FROM C c
|
||||
JOIN R r ON r.Fila = c.Fila
|
||||
JOIN K k ON k.Colonna = c.Colonna
|
||||
LEFT JOIN S s ON s.ID = c.ID
|
||||
LEFT JOIN U ON U.ID = c.ID
|
||||
ORDER BY r.RowN, k.ColN;
|
||||
"""
|
||||
def _ok(res):
|
||||
# ignora risposte superate
|
||||
if req_id < self._last_req:
|
||||
return
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
if not rows:
|
||||
# mostra matrice vuota senza rimuovere il frame (evita "schermo bianco")
|
||||
self._rebuild_matrix(0, 0, [], [], [], [], [], corsia)
|
||||
self._refresh_stats()
|
||||
self._busy.hide()
|
||||
return
|
||||
max_r = max_c = 0
|
||||
for row in rows:
|
||||
rown, coln = row[0], row[1]
|
||||
if rown and coln:
|
||||
max_r = max(max_r, int(rown))
|
||||
max_c = max(max_c, int(coln))
|
||||
mat = [[0] * max_c for _ in range(max_r)]
|
||||
fila = [[""] * max_c for _ in range(max_r)]
|
||||
col = [[""] * max_c for _ in range(max_r)]
|
||||
desc = [[""] * max_c for _ in range(max_r)]
|
||||
udc = [[""] * max_c for _ in range(max_r)]
|
||||
for row in rows:
|
||||
rown, coln, stato, descr, fila_txt, col_txt, first_udc = row
|
||||
r = int(rown) - 1
|
||||
c = int(coln) - 1
|
||||
mat[r][c] = int(stato)
|
||||
fila[r][c] = str(fila_txt or "")
|
||||
col[r][c] = str(col_txt or "")
|
||||
desc[r][c] = str(descr or f"{corsia}.{col_txt}.{fila_txt}")
|
||||
udc[r][c] = str(first_udc or "")
|
||||
self._rebuild_matrix(max_r, max_c, mat, fila, col, desc, udc, corsia)
|
||||
self._refresh_stats()
|
||||
self._busy.hide()
|
||||
def _err(ex):
|
||||
if req_id < self._last_req:
|
||||
return
|
||||
self._busy.hide()
|
||||
messagebox.showerror("Errore", f"Caricamento matrice {corsia} fallito:\n{ex}")
|
||||
self._async.run(self.db.query_json(sql, {"corsia": corsia}), _ok, _err, busy=self._busy, message=f"Carico corsia {corsia}…")
|
||||
|
||||
# ---------------- SEARCH ----------------
|
||||
def _search_udc(self):
|
||||
barcode = (self.search_var.get() or "").strip()
|
||||
if not barcode:
|
||||
self._toast("Inserisci un barcode UDC da cercare.")
|
||||
return
|
||||
|
||||
# bump token per impedire che una vecchia _load_matrix cancelli UI
|
||||
self._req_counter += 1
|
||||
search_req_id = self._req_counter
|
||||
self._last_req = search_req_id
|
||||
|
||||
sql = """
|
||||
SELECT TOP (1)
|
||||
RTRIM(c.Corsia) AS Corsia,
|
||||
RTRIM(c.Colonna) AS Colonna,
|
||||
RTRIM(c.Fila) AS Fila,
|
||||
c.ID AS IDCella
|
||||
FROM dbo.XMag_GiacenzaPallet g
|
||||
JOIN dbo.Celle c ON c.ID = g.IDCella
|
||||
WHERE g.BarcodePallet = :barcode
|
||||
AND c.ID <> 9999 AND RTRIM(c.Corsia) <> '7G'
|
||||
"""
|
||||
def _ok(res):
|
||||
if search_req_id < self._last_req:
|
||||
return
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
if not rows:
|
||||
messagebox.showinfo("Ricerca", f"UDC {barcode} non trovata.", parent=self)
|
||||
return
|
||||
corsia, col, fila, _idc = rows[0]
|
||||
corsia = str(corsia).strip(); col = str(col).strip(); fila = str(fila).strip()
|
||||
self._pending_focus = (corsia, col, fila, barcode)
|
||||
|
||||
# sincronizza listbox e carica SEMPRE la corsia della UDC
|
||||
self._select_corsia_in_listbox(corsia)
|
||||
self.corsia_selezionata.set(corsia)
|
||||
self._load_matrix(corsia) # highlight avverrà in _rebuild_matrix
|
||||
def _err(ex):
|
||||
if search_req_id < self._last_req:
|
||||
return
|
||||
messagebox.showerror("Ricerca", f"Errore ricerca UDC:\n{ex}", parent=self)
|
||||
|
||||
self._async.run(self.db.query_json(sql, {"barcode": barcode}), _ok, _err, busy=self._busy, message="Cerco UDC…")
|
||||
|
||||
def _try_highlight(self, col_txt: str, fila_txt: str) -> bool:
|
||||
for r in range(len(self.col_txt)):
|
||||
for c in range(len(self.col_txt[r])):
|
||||
if self.col_txt[r][c] == col_txt and self.fila_txt[r][c] == fila_txt:
|
||||
self._clear_highlight()
|
||||
btn = self.buttons[r][c]
|
||||
btn.configure(border_width=3, border_color="blue")
|
||||
try:
|
||||
fr = self.btn_frames[r][c]
|
||||
fr.configure(border_color="blue", border_width=2)
|
||||
except Exception:
|
||||
pass
|
||||
self._highlighted = (r, c)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _highlight_cell_by_labels(self, col_txt: str, fila_txt: str):
|
||||
if not self._try_highlight(col_txt, fila_txt):
|
||||
self._toast("Cella trovata ma non mappabile a pulsante.")
|
||||
|
||||
# ---------------- COMMANDS ----------------
|
||||
def _refresh_current(self):
|
||||
if self.corsia_selezionata.get():
|
||||
self._load_matrix(self.corsia_selezionata.get())
|
||||
|
||||
def _export_xlsx(self):
|
||||
if not self.matrix_state:
|
||||
messagebox.showinfo("Export", "Nessuna matrice da esportare.")
|
||||
return
|
||||
corsia = self.corsia_selezionata.get() or "NA"
|
||||
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
|
||||
default = f"layout_matrice_{corsia}_{ts}.xlsx"
|
||||
path = filedialog.asksaveasfilename(
|
||||
title="Esporta matrice",
|
||||
defaultextension=".xlsx",
|
||||
initialfile=default,
|
||||
filetypes=[("Excel", "*.xlsx")]
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import PatternFill, Alignment, Font
|
||||
except Exception as ex:
|
||||
messagebox.showerror("Export", f"Manca openpyxl: {ex}\nInstalla con: pip install openpyxl")
|
||||
return
|
||||
rows = len(self.matrix_state)
|
||||
cols = len(self.matrix_state[0]) if self.matrix_state else 0
|
||||
wb = Workbook()
|
||||
ws1 = wb.active
|
||||
ws1.title = f"Dettaglio {corsia}"
|
||||
ws1.append(["Corsia", "FilaIdx", "ColIdx", "Stato", "Descrizione", "FilaTxt", "ColTxt", "UDC1"])
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
st = self.matrix_state[r][c]
|
||||
stato_lbl = "Vuota" if st == 0 else ("Piena" if st == 1 else "Doppia")
|
||||
ws1.append([corsia, r + 1, c + 1, stato_lbl,
|
||||
self.desc[r][c], self.fila_txt[r][c], self.col_txt[r][c], self.udc1[r][c]])
|
||||
for cell in ws1[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
ws2 = wb.create_sheet(f"Matrice {corsia}")
|
||||
fills = {
|
||||
0: PatternFill("solid", fgColor="B0B0B0"),
|
||||
1: PatternFill("solid", fgColor="FFA500"),
|
||||
2: PatternFill("solid", fgColor="D62728"),
|
||||
}
|
||||
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
value = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}\n{self.udc1[r][c]}"
|
||||
cell = ws2.cell(row=(rows - r), column=c + 1, value=value) # capovolto per avere 'a' in basso
|
||||
cell.fill = fills.get(self.matrix_state[r][c], fills[0])
|
||||
cell.alignment = center
|
||||
try:
|
||||
wb.save(path)
|
||||
self._toast(f"Esportato: {path}")
|
||||
except Exception as ex:
|
||||
messagebox.showerror("Export", f"Salvataggio fallito:\n{ex}")
|
||||
|
||||
# ---------------- STATS ----------------
|
||||
def _refresh_stats(self):
|
||||
# globale dal DB
|
||||
sql_tot = """
|
||||
WITH C AS (
|
||||
SELECT ID
|
||||
FROM dbo.Celle
|
||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||
AND LTRIM(RTRIM(Corsia)) <> '7G'
|
||||
AND LTRIM(RTRIM(Fila)) IS NOT NULL
|
||||
AND LTRIM(RTRIM(Colonna)) IS NOT NULL
|
||||
),
|
||||
S AS (
|
||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
||||
FROM C AS c LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
||||
GROUP BY c.ID
|
||||
)
|
||||
SELECT
|
||||
CAST(SUM(CASE WHEN s.n>0 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercPieno,
|
||||
CAST(SUM(CASE WHEN s.n>1 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercDoppie
|
||||
FROM C LEFT JOIN S s ON s.ID = C.ID;
|
||||
"""
|
||||
def _ok(res):
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
p_full = float(rows[0][0] or 0.0) if rows else 0.0
|
||||
p_dbl = float(rows[0][1] or 0.0) if rows else 0.0
|
||||
self._draw_bar(self.tot_canvas, p_full)
|
||||
self.tot_text.configure(text=pct_text(p_full, p_dbl))
|
||||
self._async.run(self.db.query_json(sql_tot, {}), _ok, lambda e: None, busy=None, message=None)
|
||||
|
||||
# selezionata dalla matrice in memoria
|
||||
if self.matrix_state:
|
||||
tot = sum(len(r) for r in self.matrix_state)
|
||||
full = sum(1 for row in self.matrix_state for v in row if v in (1, 2))
|
||||
doubles = sum(1 for row in self.matrix_state for v in row if v == 2)
|
||||
p_full = (full / tot) if tot else 0.0
|
||||
p_dbl = (doubles / tot) if tot else 0.0
|
||||
else:
|
||||
p_full = p_dbl = 0.0
|
||||
self._draw_bar(self.sel_canvas, p_full)
|
||||
self.sel_text.configure(text=pct_text(p_full, p_dbl))
|
||||
|
||||
def _draw_bar(self, cv: tk.Canvas, p_full: float):
|
||||
cv.delete("all")
|
||||
w = max(300, cv.winfo_width() or 600)
|
||||
h = 18
|
||||
fw = int(w * max(0.0, min(1.0, p_full)))
|
||||
cv.create_rectangle(2, 2, 2 + fw, 2 + h, fill="#D62728", width=0)
|
||||
cv.create_rectangle(2 + fw, 2, 2 + w, 2 + h, fill="#2CA02C", width=0)
|
||||
cv.create_rectangle(2, 2, 2 + w, 2 + h, outline="#555", width=1)
|
||||
|
||||
# ---------------- UTIL ----------------
|
||||
def _toast(self, msg, ms=1400):
|
||||
if not hasattr(self, "_status"):
|
||||
self._status = ctk.CTkLabel(self, anchor="w")
|
||||
self._status.grid(row=3, column=0, sticky="ew")
|
||||
self._status.configure(text=msg)
|
||||
self.after(ms, lambda: self._status.configure(text=""))
|
||||
|
||||
def _copy(self, txt: str):
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(txt)
|
||||
self._toast(f"Copiato: {txt}")
|
||||
|
||||
|
||||
def open_layout_window(parent, db_app):
|
||||
key = "_layout_window_singleton"
|
||||
ex = getattr(parent, key, None)
|
||||
if ex and ex.winfo_exists():
|
||||
try:
|
||||
ex.lift()
|
||||
ex.focus_force()
|
||||
return ex
|
||||
except Exception:
|
||||
pass
|
||||
w = LayoutWindow(parent, db_app)
|
||||
setattr(parent, key, w)
|
||||
return w
|
||||
Reference in New Issue
Block a user