Files
ware_house/search_pallets.py
2025-10-27 17:18:09 +01:00

454 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# search_pallets.py
# Finestra di ricerca su UDC / Lotto / Codice Prodotto
# - Tre campi: UDC, Lotto, Codice Prodotto (AND tra i campi valorizzati)
# - Ricerca su TUTTE le celle, incluse IDCella=9999 e corsia 7G
# - Risultati in una griglia: IDCella, Ubicazione, UDC, Lotto, Codice, Descrizione
# - Se la ricerca restituisce >0 righe, i campi input vengono svuotati
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
from tkinter import filedialog
# opzionale export xlsx
try:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
_HAS_XLSX = True
except Exception:
_HAS_XLSX = False
# opzionale: usare tksheet per avere griglie con bordi di cella
try:
from tksheet import Sheet
except Exception:
Sheet = None
SQL_SEARCH = r"""
WITH BASE AS (
SELECT
g.IDCella,
-- forza stringa per ricerche LIKE
CONCAT(g.BarcodePallet, '') AS UDC,
c.Corsia,
c.Colonna,
c.Fila
FROM dbo.XMag_GiacenzaPallet AS g
LEFT JOIN dbo.Celle AS c ON c.ID = g.IDCella
-- NB: qui NON escludiamo IDCella=9999 né '7G'
),
JOINED AS (
SELECT
b.IDCella,
b.UDC,
b.Corsia,
b.Colonna,
b.Fila,
t.Lotto,
t.Prodotto,
t.Descrizione
FROM BASE b
LEFT JOIN dbo.vXTracciaProdotti AS t
ON t.Pallet COLLATE Latin1_General_CI_AS = LEFT(b.UDC, 6) COLLATE Latin1_General_CI_AS
)
SELECT
j.IDCella,
UPPER(
CONCAT(
COALESCE(LTRIM(RTRIM(j.Corsia)), 'NA'), '.',
COALESCE(LTRIM(RTRIM(CAST(j.Colonna AS varchar(32)))), 'NA'), '.',
COALESCE(LTRIM(RTRIM(CAST(j.Fila AS varchar(32)))), 'NA')
)
) AS Ubicazione,
j.UDC,
j.Lotto,
j.Prodotto,
j.Descrizione
FROM JOINED j
WHERE 1=1
AND ( :udc IS NULL OR j.UDC COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%') )
AND ( :lotto IS NULL OR j.Lotto COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :lotto, '%') )
AND ( :codice IS NULL OR j.Prodotto COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :codice, '%') )
ORDER BY
CASE WHEN j.IDCella = 9999 THEN 1 ELSE 0 END,
j.Corsia, j.Colonna, j.Fila, j.UDC, j.Lotto, j.Prodotto;
"""
class SearchWindow(ctk.CTkToplevel):
def __init__(self, parent: tk.Widget, db_app):
super().__init__(parent)
self.title("Warehouse · Ricerca UDC/Lotto/Codice")
self.geometry("1100x720")
self.minsize(900, 560)
self.resizable(True, True)
self.db = db_app
self._busy = BusyOverlay(self)
self._async = AsyncRunner(self)
# stato ordinamento colonne (col -> reverse bool)
self._sort_state: dict[str, bool] = {}
self._build_ui()
def _build_ui(self):
# layout griglia principale
self.grid_rowconfigure(0, weight=0)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
# --- barra di ricerca ---
top = ctk.CTkFrame(self)
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
for i in range(8):
top.grid_columnconfigure(i, weight=0)
top.grid_columnconfigure(7, weight=1)
ctk.CTkLabel(top, text="UDC:").grid(row=0, column=0, sticky="w")
self.var_udc = tk.StringVar()
e_udc = ctk.CTkEntry(top, textvariable=self.var_udc, width=160)
e_udc.grid(row=0, column=1, sticky="w", padx=(4, 12))
ctk.CTkLabel(top, text="Lotto:").grid(row=0, column=2, sticky="w")
self.var_lotto = tk.StringVar()
e_lotto = ctk.CTkEntry(top, textvariable=self.var_lotto, width=140)
e_lotto.grid(row=0, column=3, sticky="w", padx=(4, 12))
ctk.CTkLabel(top, text="Codice prodotto:").grid(row=0, column=4, sticky="w")
self.var_codice = tk.StringVar()
e_cod = ctk.CTkEntry(top, textvariable=self.var_codice, width=160)
e_cod.grid(row=0, column=5, sticky="w", padx=(4, 12))
btn = ctk.CTkButton(top, text="Cerca", command=self._do_search)
btn.grid(row=0, column=6, sticky="w")
btn_exp = ctk.CTkButton(top, text="Esporta XLSX", command=self._export_xlsx)
btn_exp.grid(row=0, column=7, sticky="e")
# --- griglia risultati ---
wrap = ctk.CTkFrame(self)
wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
wrap.grid_rowconfigure(0, weight=1)
wrap.grid_columnconfigure(0, weight=1)
# forza modalità Treeview (niente tksheet) per stabilità
self.use_sheet = False
cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione")
self.tree = ttk.Treeview(wrap, columns=cols, show="headings")
# stile: zebra + header leggibile
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")
# tag per righe alternate + id9999 evidenziate
self.tree.tag_configure("even", background="#FFFFFF")
self.tree.tag_configure("odd", background="#F7F9FC")
# evidenzia spediti (IDCella=9999) in rosato tenue
self.tree.tag_configure("id9999", background="#FFECEC", foreground="#B00020")
sy = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
sx = ttk.Scrollbar(wrap, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
self.tree.grid(row=0, column=0, sticky="nsew")
sy.grid(row=0, column=1, sticky="ns")
sx.grid(row=1, column=0, sticky="ew")
# doppio click → copia UDC; gestione click header
self.tree.bind("<Double-1>", self._on_dclick)
self.tree.bind("<Button-1>", self._maybe_handle_heading_click, add=True)
self.tree.bind("<Double-1>", self._on_heading_double_click, add=True)
def _apply_zebra(self):
for i, iid in enumerate(self.tree.get_children("")):
vals = self.tree.item(iid, "values")
zebra = "even" if i % 2 == 0 else "odd"
is9999 = False
if vals:
try:
is9999 = int(vals[0]) == 9999
except Exception:
is9999 = False
tags = ("id9999", zebra) if is9999 else (zebra,)
self.tree.item(iid, tags=tags)
# ---------------- AZIONI ----------------
def _export_xlsx(self):
# raccogli dati dalla griglia
rows = []
for iid in self.tree.get_children(""):
rows.append(self.tree.item(iid, "values"))
if not rows:
messagebox.showinfo("Esporta", "Non ci sono righe da esportare.", parent=self)
return
if not _HAS_XLSX:
messagebox.showerror("Esporta", "Per l'esportazione serve 'openpyxl' (pip install openpyxl).", parent=self)
return
# dialog salvataggio
from datetime import datetime
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
default_name = f"esportazione_ricerca_{ts}.xlsx"
fname = filedialog.asksaveasfilename(parent=self, title="Esporta in Excel",
defaultextension=".xlsx",
filetypes=[("Excel Workbook","*.xlsx")],
initialfile=default_name)
if not fname:
return
# crea workbook e scrivi
try:
wb = Workbook()
ws = wb.active
ws.title = "Risultati"
headers = ("IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione")
for j, h in enumerate(headers, start=1):
c = ws.cell(row=1, column=j, value=h)
c.font = Font(bold=True)
c.alignment = Alignment(horizontal="center", vertical="center")
r = 2
for row in rows:
for j, v in enumerate(row, start=1):
ws.cell(row=r, column=j, value=v)
r += 1
# autosize
widths = {}
for row in ws.iter_rows(values_only=True):
for j, val in enumerate(row, start=1):
s = "" if val is None else str(val)
widths[j] = max(widths.get(j, 0), len(s))
from openpyxl.utils import get_column_letter
for j, w in widths.items():
ws.column_dimensions[get_column_letter(j)].width = min(max(w + 2, 10), 60)
wb.save(fname)
messagebox.showinfo("Esporta", f"File creato:\n{fname}", parent=self)
except Exception as ex:
messagebox.showerror("Esporta", f"Errore durante l'esportazione:{ex}", parent=self)
def _on_dclick(self, evt):
# copia UDC solo se il doppio click avviene su una cella, non sull'header
region = self.tree.identify("region", evt.x, evt.y)
if region != "cell":
return
sel = self.tree.focus()
if not sel:
return
vals = self.tree.item(sel, "values")
if len(vals) >= 3 and vals[2]: # UDC
try:
self.clipboard_clear()
self.clipboard_append(vals[2])
except Exception:
pass
# --------------- ORDINAMENTO COLONNE ---------------
def _maybe_handle_heading_click(self, evt):
# evita che il click sulle intestazioni selezioni una riga fantasma
region = self.tree.identify("region", evt.x, evt.y)
if region == "heading":
return "break"
def _on_heading_double_click(self, evt):
# doppio click su intestazione: ordina la colonna corrispondente
region = self.tree.identify("region", evt.x, evt.y)
if region != "heading":
return
col_id = self.tree.identify_column(evt.x) # es. '#1'
try:
idx = int(col_id.replace('#','')) - 1
except Exception:
return "break"
cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione")
if 0 <= idx < len(cols):
self._sort_by_column(cols[idx])
return "break"
def _sort_key_for_col(self, col: str, val: str):
if val is None:
return (1, "") # None in fondo
s = str(val)
if col in ("IDCella",):
# prova numero
try:
return (0, int(s))
except Exception:
return (0, s.lower())
if col in ("Lotto", "Codice", "UDC"):
return (0, s.lower())
return (0, s.lower())
def _sort_by_column(self, col: str):
try:
# toggle reverse
rev = self._sort_state.get(col, False)
self._sort_state[col] = not rev
# raccogli dati correnti
rows = []
for iid in self.tree.get_children(""):
vals = self.tree.item(iid, "values")
row = {"iid": iid,
"IDCella": vals[0] if len(vals) > 0 else None,
"Ubicazione": vals[1] if len(vals) > 1 else None,
"UDC": vals[2] if len(vals) > 2 else None,
"Lotto": vals[3] if len(vals) > 3 else None,
"Codice": vals[4] if len(vals) > 4 else None,
"Descrizione": vals[5] if len(vals) > 5 else None}
rows.append(row)
rows.sort(key=lambda r: self._sort_key_for_col(col, r.get(col)), reverse=rev)
def _apply_moves():
for index, r in enumerate(rows):
self.tree.move(r["iid"], "", index)
# aggiorna indicatori visuali nelle heading (▲/▼)
for k in ("IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione"):
base = {
"IDCella": "IDCella",
"Ubicazione": "Ubicazione",
"UDC": "UDC / Barcode",
"Lotto": "Lotto",
"Codice": "Codice prodotto",
"Descrizione": "Descrizione prodotto",
}[k]
if k == col:
arrow = "" if not rev else ""
self.tree.heading(k, text=base + arrow)
else:
self.tree.heading(k, text=base)
self._apply_zebra()
# posticipa le move per evitare reentrancy su doppio click
self.after_idle(_apply_moves)
except Exception:
# in caso di problemi non lasciamo la finestra in stato incoerente
pass
# --- ordinamento per tksheet (doppio click sui titoli) ---
def _on_sheet_header_double_click(self, event_dict):
try:
c = event_dict.get("column")
except Exception:
return
if c is None:
return
headers = ["IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione"]
if not (0 <= c < len(headers)):
return
colname = headers[c]
rev = self._sort_state.get(colname, False)
self._sort_state[colname] = not rev
data = self.sheet.get_sheet_data(return_copy=True)
def keyf(row):
val = row[c] if c < len(row) else None
if val is None:
return (1, "")
s = str(val)
if colname == "IDCella":
try:
return (0, int(s))
except Exception:
return (0, s.lower())
return (0, s.lower())
data.sort(key=keyf, reverse=rev)
self.sheet.set_sheet_data(data)
# evidenzia l'header ordinato (semplice: cambia testo temporaneamente)
try:
arrow = "" if not rev else ""
hdrs = list(headers)
hdrs[c] = hdrs[c] + arrow
self.sheet.headers(hdrs)
except Exception:
pass
def _do_search(self):
udc = (self.var_udc.get() or "").strip()
lotto = (self.var_lotto.get() or "").strip()
codice = (self.var_codice.get() or "").strip()
# Se nessun filtro, chiedi conferma (evita estrazione enorme non voluta)
if not (udc or lotto or codice):
if not messagebox.askyesno(
"Conferma",
"Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?",
parent=self,
):
return
# Parametri: passa NULL se campo vuoto -> i filtri "si spengono"
params = {
"udc": (udc if udc else None),
"lotto": (lotto if lotto else None),
"codice": (codice if codice else None),
}
def _ok(res):
rows = res.get("rows", []) if isinstance(res, dict) else []
# --- popola UI ---
if self.use_sheet:
try:
data = []
for r in rows:
idc, ubi, udc_v, lot_v, cod_v, desc_v = r
data.append([idc, ubi, udc_v, lot_v, cod_v, desc_v])
self.sheet.set_sheet_data(data)
self.sheet.set_all_cell_sizes_to_text()
except Exception as ex:
# fallback di sicurezza su Treeview
self.use_sheet = False
if not self.use_sheet:
# Treeview
for iid in self.tree.get_children():
self.tree.delete(iid)
for idx, r in enumerate(rows):
idc, ubi, udc_v, lot_v, cod_v, desc_v = r
zebra = "even" if idx % 2 == 0 else "odd"
try:
is9999 = int(idc) == 9999
except Exception:
is9999 = False
tags = ("id9999", zebra) if is9999 else (zebra,)
self.tree.insert("", "end", values=(idc, ubi, udc_v, lot_v, cod_v, desc_v), tags=tags)
# --- feedback utente ---
if not rows:
messagebox.showinfo(
"Nessun risultato",
"Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
parent=self,
)
else:
# reset campi se risultato non vuoto
self.var_udc.set("")
self.var_lotto.set("")
self.var_codice.set("")
self._busy.hide()
def _err(ex):
self._busy.hide()
messagebox.showerror("Errore ricerca", str(ex), parent=self)
self._async.run(self.db.query_json(SQL_SEARCH, params), _ok, _err, busy=self._busy, message="Cerco…")
def open_search_window(parent, db_app):
key = "_search_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 = SearchWindow(parent, db_app)
setattr(parent, key, w)
return w