"""Read-only picking-list history window.""" from __future__ import annotations from datetime import date, datetime, timedelta import tkinter as tk from tkinter import messagebox, ttk from typing import Any import customtkinter as ctk 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 window_placement import place_window_fullsize_below_parent_later SQL_STORICO_PL = """ WITH base AS ( SELECT * FROM dbo.py_XMag_ViewPackingListStorico ), agg 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 ) SELECT Documento, DataDocumento, StatoDocumento, NAZIONE, CodNazione, TotUDC, RigheResidue, RigheSpedite, RigheTotali, CASE WHEN StatoDocumento = 'D' THEN 'Chiusa' WHEN RigheResidue = 0 THEN 'Esaurita' WHEN RigheSpedite > 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; """ SQL_STORICO_PL_DETAILS = """ SELECT Documento, Pallet, Lotto, Articolo, Descrizione, Qta, DataDocumento, StatoDocumento, Cella, Ubicazione, Ordinamento, IDStato FROM dbo.py_XMag_ViewPackingListStorico WHERE Documento = :documento ORDER BY Ordinamento, Pallet; """ def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(res, dict): return [] rows = res.get("rows") or [] cols = res.get("columns") or [] if rows and isinstance(rows[0], dict): return [row for row in rows if isinstance(row, dict)] out: list[dict[str, Any]] = [] for row in rows: if isinstance(row, (list, tuple)) and cols: out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))}) return out def _format_date(value: Any) -> str: """Format SQL Server/SAMA date values into dd/mm/yyyy for operators.""" if value in (None, ""): return "" if isinstance(value, datetime): return value.strftime("%d/%m/%Y") if isinstance(value, date): return value.strftime("%d/%m/%Y") if isinstance(value, (int, float)): try: # SQL Server numeric datetime: CAST(datetime AS int), day 0 = 1900-01-01. return (datetime(1900, 1, 1) + timedelta(days=int(value))).strftime("%d/%m/%Y") except Exception: return str(value) text = str(value).strip() if not text: return "" if text.isdigit() and len(text) == 8: try: return datetime.strptime(text, "%Y%m%d").strftime("%d/%m/%Y") except ValueError: pass try: return datetime.fromisoformat(text.replace("Z", "+00:00")).strftime("%d/%m/%Y") except Exception: return text class StoricoPickingListWindow(ctk.CTkToplevel): """Window that shows historical picking-list status and details.""" def __init__(self, parent: tk.Widget, db_client, session=None): super().__init__(parent) self.db_client = db_client self.session = session self._theme = theme_section("history_picking_window", theme_section("pickinglist_window", {})) self._locale_catalog = load_locale_catalog() self._async = AsyncRunner(self) self._busy = InlineBusyOverlay(self, self._theme) self.var_documento = tk.StringVar() self.title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List")) self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720"))) minsize = theme_value(self._theme, "window_minsize", [980, 560]) self.minsize(int(minsize[0]), int(minsize[1])) try: self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f"))) except Exception: pass self._build_ui() self.after(300, self._load_master) def _build_ui(self) -> None: self.grid_rowconfigure(1, weight=1) self.grid_rowconfigure(3, weight=1) self.grid_columnconfigure(0, weight=1) top = ctk.CTkFrame( self, fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")), ) top.grid(row=0, column=0, sticky="ew", padx=8, pady=8) top.grid_columnconfigure(3, weight=1) label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)) entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10)) button_font = theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")) ctk.CTkLabel(top, text="Documento:", font=label_font).grid(row=0, column=0, sticky="w") ctk.CTkEntry(top, textvariable=self.var_documento, width=140, font=entry_font).grid( row=0, column=1, sticky="w", padx=(4, 12) ) ctk.CTkButton( top, text=loc_text("history.picking.button.reload", catalog=self._locale_catalog, default="Ricarica"), command=self._load_master, font=button_font, ).grid( row=0, column=2, sticky="w" ) self.master_tree = self._make_tree( row=1, columns=("Documento", "Data", "StatoDoc", "NAZIONE", "TotUDC", "Residue", "Spedite", "Stato", "IDStato"), widths={ "Documento": 100, "Data": 110, "StatoDoc": 75, "NAZIONE": 260, "TotUDC": 90, "Residue": 90, "Spedite": 90, "Stato": 120, "IDStato": 80, }, ) self.master_tree.bind("<>", self._on_master_select) ctk.CTkLabel( self, text=loc_text("history.picking.detail_title", catalog=self._locale_catalog, default="Dettaglio contenuto"), anchor="w", font=button_font, ).grid( row=2, column=0, sticky="ew", padx=8, pady=(4, 2) ) self.detail_tree = self._make_tree( row=3, columns=("Pallet", "Lotto", "Articolo", "Descrizione", "Qta", "Data", "StatoDoc", "Cella", "Ubicazione", "Ordine"), widths={ "Pallet": 120, "Lotto": 120, "Articolo": 130, "Descrizione": 320, "Qta": 80, "Data": 110, "StatoDoc": 75, "Cella": 80, "Ubicazione": 180, "Ordine": 80, }, ) def _make_tree(self, *, row: int, columns: tuple[str, ...], widths: dict[str, int]) -> ttk.Treeview: wrap = ctk.CTkFrame(self) wrap.grid(row=row, column=0, sticky="nsew", padx=8, pady=(0, 8)) wrap.grid_rowconfigure(0, weight=1) wrap.grid_columnconfigure(0, weight=1) tree = ttk.Treeview(wrap, columns=columns, show="headings") for col in columns: tree.heading(col, text=col) tree.column(col, width=widths.get(col, 100), anchor="w") tree.tag_configure("done", background="#ECECEC") tree.tag_configure("active", background="#EAF7EA") 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) tree.grid(row=0, column=0, sticky="nsew") sy.grid(row=0, column=1, sticky="ns") sx.grid(row=1, column=0, sticky="ew") return tree def _load_master(self) -> None: params = {"documento": str(self.var_documento.get() or "").strip() or None} async def _job(): return await self.db_client.query_json(SQL_STORICO_PL, params) def _ok(res): self._fill_master(_rows_to_dicts(res)) def _err(ex): messagebox.showerror( loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"), loc_text( "history.picking.msg.load_error", catalog=self._locale_catalog, default="Errore caricamento:\n{error}", ).format(error=ex), parent=self, ) self._async.run( _job(), _ok, _err, busy=self._busy, message=loc_text( "history.picking.busy.master", catalog=self._locale_catalog, default="Carico storico picking list...", ), ) def _fill_master(self, rows: list[dict[str, Any]]) -> None: self.master_tree.delete(*self.master_tree.get_children("")) 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 "" self.master_tree.insert( "", "end", iid=f"doc_{row.get('Documento')}_{index}", values=( row.get("Documento", ""), _format_date(row.get("DataDocumento", "")), row.get("StatoDocumento", ""), row.get("NAZIONE", ""), row.get("TotUDC", ""), row.get("RigheResidue", ""), row.get("RigheSpedite", ""), stato, row.get("IDStato", ""), ), tags=(tag,) if tag else (), ) def _on_master_select(self, _event=None) -> None: selected = self.master_tree.selection() if not selected: return values = self.master_tree.item(selected[0], "values") if not values: return documento = values[0] async def _job(): return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento}) def _ok(res): self._fill_detail(_rows_to_dicts(res)) def _err(ex): messagebox.showerror( loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"), loc_text( "history.picking.msg.detail_error", catalog=self._locale_catalog, default="Errore dettaglio:\n{error}", ).format(error=ex), parent=self, ) self._async.run( _job(), _ok, _err, busy=self._busy, message=loc_text( "history.picking.busy.detail", catalog=self._locale_catalog, default="Carico dettaglio picking list...", ), ) def _fill_detail(self, rows: list[dict[str, Any]]) -> None: self.detail_tree.delete(*self.detail_tree.get_children("")) for row in rows: done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999 self.detail_tree.insert( "", "end", values=( row.get("Pallet", ""), row.get("Lotto", ""), row.get("Articolo", ""), row.get("Descrizione", ""), row.get("Qta", ""), _format_date(row.get("DataDocumento", "")), row.get("StatoDocumento", ""), row.get("Cella", ""), row.get("Ubicazione", ""), row.get("Ordinamento", ""), ), tags=("done",) if done else (), ) def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc: """Open the picking-list history window.""" win = StoricoPickingListWindow(parent, db_client, session=session) place_window_fullsize_below_parent_later(parent, win) win.bind("", lambda _e: win.destroy()) return win