migrazione verso gitea

This commit is contained in:
2026-03-31 19:15:33 +02:00
parent 8806d598eb
commit f6a5b1b29f
118 changed files with 17197 additions and 459 deletions

View File

@@ -1,16 +1,19 @@
# view_celle_multiple.py
"""Exploration window for cells containing more than one pallet."""
import json
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import customtkinter as ctk
from datetime import datetime
from tkinter import filedialog, messagebox, ttk
import customtkinter as ctk
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
from openpyxl.styles import Alignment, Font
from gestione_aree_frame_async import AsyncRunner
def _json_obj(res):
"""Normalize raw DB responses into a dictionary with a ``rows`` key."""
if isinstance(res, str):
try:
res = json.loads(res)
@@ -22,6 +25,7 @@ def _json_obj(res):
raise RuntimeError(f"{err}\n{detail}")
return res if isinstance(res, dict) else {"rows": res}
UBI_B = (
"UPPER("
" CONCAT("
@@ -128,11 +132,17 @@ FROM unione
ORDER BY Ord, Corsia;
"""
class CelleMultipleWindow(ctk.CTkToplevel):
"""Tree-based explorer for duplicated pallet allocations."""
def __init__(self, root, db_client, runner: AsyncRunner | None = None):
"""Bind the shared DB client and immediately load the tree summary."""
super().__init__(root)
self.title("Celle con più pallet")
self.geometry("1100x700"); self.minsize(900,550); self.resizable(True, True)
self.title("Celle con piu' pallet")
self.geometry("1100x700")
self.minsize(900, 550)
self.resizable(True, True)
self.db = db_client
self.runner = runner or AsyncRunner(self)
@@ -142,205 +152,280 @@ class CelleMultipleWindow(ctk.CTkToplevel):
self.refresh_all()
def _build_layout(self):
"""Create the toolbar, lazy-loaded tree and percentage summary table."""
self.grid_rowconfigure(0, weight=5)
self.grid_rowconfigure(1, weight=70)
self.grid_rowconfigure(2, weight=25, minsize=160)
self.grid_columnconfigure(0, weight=1)
toolbar = ctk.CTkFrame(self); toolbar.grid(row=0, column=0, sticky="nsew")
toolbar = ctk.CTkFrame(self)
toolbar.grid(row=0, column=0, sticky="nsew")
ctk.CTkButton(toolbar, text="Aggiorna", command=self.refresh_all).pack(side="left", padx=6, pady=4)
ctk.CTkButton(toolbar, text="Espandi tutto", command=self.expand_all).pack(side="left", padx=6, pady=4)
ctk.CTkButton(toolbar, text="Comprimi tutto", command=self.collapse_all).pack(side="left", padx=6, pady=4)
ctk.CTkButton(toolbar, text="Esporta in XLSX", command=self.export_to_xlsx).pack(side="left", padx=6, pady=4)
f = ctk.CTkFrame(self); f.grid(row=1, column=0, sticky="nsew", padx=6, pady=(0,6))
f.grid_rowconfigure(0, weight=1); f.grid_columnconfigure(0, weight=1)
self.tree = ttk.Treeview(f, columns=("col2","col3"), show="tree headings", selectmode="browse")
self.tree.heading("#0", text="Nodo"); self.tree.heading("col2", text="Descrizione"); self.tree.heading("col3", text="Lotto")
y = ttk.Scrollbar(f, orient="vertical", command=self.tree.yview)
x = ttk.Scrollbar(f, orient="horizontal", command=self.tree.xview)
frame = ctk.CTkFrame(self)
frame.grid(row=1, column=0, sticky="nsew", padx=6, pady=(0, 6))
frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)
self.tree = ttk.Treeview(frame, columns=("col2", "col3"), show="tree headings", selectmode="browse")
self.tree.heading("#0", text="Nodo")
self.tree.heading("col2", text="Descrizione")
self.tree.heading("col3", text="Lotto")
y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set)
self.tree.grid(row=0, column=0, sticky="nsew"); y.grid(row=0, column=1, sticky="ns"); x.grid(row=1, column=0, sticky="ew")
self.tree.grid(row=0, column=0, sticky="nsew")
y.grid(row=0, column=1, sticky="ns")
x.grid(row=1, column=0, sticky="ew")
sumf = ctk.CTkFrame(self)
sumf.grid(row=2, column=0, sticky="nsew", padx=6, pady=(0,6))
sumf.grid(row=2, column=0, sticky="nsew", padx=6, pady=(0, 6))
ctk.CTkLabel(sumf, text="Riepilogo % celle multiple per corsia", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=8, pady=(8, 0))
inner = ctk.CTkFrame(sumf)
inner.pack(fill="both", expand=True, padx=6, pady=6)
inner.grid_rowconfigure(0, weight=1); inner.grid_columnconfigure(0, weight=1)
self.sum_tbl = ttk.Treeview(inner, columns=("Corsia","TotCelle","CelleMultiple","Percentuale"), show="headings")
for k,t,w,a in (("Corsia","Corsia",100,"center"),
("TotCelle","Totale celle",120,"e"),
("CelleMultiple",">1 UDC",120,"e"),
("Percentuale","%",80,"e")):
self.sum_tbl.heading(k, text=t); self.sum_tbl.column(k, width=w, anchor=a)
inner.grid_rowconfigure(0, weight=1)
inner.grid_columnconfigure(0, weight=1)
self.sum_tbl = ttk.Treeview(inner, columns=("Corsia", "TotCelle", "CelleMultiple", "Percentuale"), show="headings")
for key, title, width, anchor in (
("Corsia", "Corsia", 100, "center"),
("TotCelle", "Totale celle", 120, "e"),
("CelleMultiple", ">1 UDC", 120, "e"),
("Percentuale", "%", 80, "e"),
):
self.sum_tbl.heading(key, text=title)
self.sum_tbl.column(key, width=width, anchor=anchor)
y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview)
x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview)
self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set)
self.sum_tbl.grid(row=0, column=0, sticky="nsew"); y2.grid(row=0, column=1, sticky="ns"); x2.grid(row=1, column=0, sticky="ew")
self.sum_tbl.grid(row=0, column=0, sticky="nsew")
y2.grid(row=0, column=1, sticky="ns")
x2.grid(row=1, column=0, sticky="ew")
def _bind_events(self):
"""Attach lazy-load behavior when nodes are expanded."""
self.tree.bind("<<TreeviewOpen>>", self._on_open_node)
def refresh_all(self):
self._load_corsie(); self._load_riepilogo()
"""Reload both the duplication tree and the summary percentage table."""
self._load_corsie()
self._load_riepilogo()
def _load_corsie(self):
"""Load root nodes representing aisles with duplicated cells."""
self.tree.delete(*self.tree.get_children())
async def _q(db): return await db.query_json(SQL_CORSIE, as_dict_rows=True)
async def _q(db):
return await db.query_json(SQL_CORSIE, as_dict_rows=True)
self.runner.run(_q(self.db), self._fill_corsie, lambda e: messagebox.showerror("Errore", str(e), parent=self))
def _fill_corsie(self, res):
"""Populate root tree nodes after the aisle query completes."""
rows = _json_obj(res).get("rows", [])
for r in rows:
corsia = r.get("Corsia");
if not corsia: continue
for row in rows:
corsia = row.get("Corsia")
if not corsia:
continue
node_id = f"corsia:{corsia}"
self.tree.insert("", "end", iid=node_id, text=f"Corsia {corsia}", values=("", ""), open=False, tags=("corsia",))
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", ""))
def _on_open_node(self, _evt):
"""Lazy-load children when a tree node is expanded."""
sel = self.tree.focus()
if not sel: return
if not sel:
return
if sel.startswith("corsia:"):
lazy_id = f"{sel}::lazy"
if lazy_id in self.tree.get_children(sel):
self.tree.delete(lazy_id)
corsia = sel.split(":",1)[1]
corsia = sel.split(":", 1)[1]
self._load_celle_for_corsia(sel, corsia)
elif sel.startswith("cella:"):
lazy_id = f"{sel}::lazy"
if lazy_id in self.tree.get_children(sel):
self.tree.delete(lazy_id)
idcella = int(sel.split(":",1)[1])
idcella = int(sel.split(":", 1)[1])
for child in self.tree.get_children(sel):
self.tree.delete(child)
self._load_pallet_for_cella(sel, idcella)
def _load_celle_for_corsia(self, parent_iid, corsia):
async def _q(db): return await db.query_json(SQL_CELLE_DUP_PER_CORSIA, params={"corsia": corsia}, as_dict_rows=True)
self.runner.run(_q(self.db), lambda res: self._fill_celle(parent_iid, res),
lambda e: messagebox.showerror("Errore", str(e), parent=self))
"""Query duplicated cells for the selected aisle."""
async def _q(db):
return await db.query_json(SQL_CELLE_DUP_PER_CORSIA, params={"corsia": corsia}, as_dict_rows=True)
self.runner.run(_q(self.db), lambda res: self._fill_celle(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self))
def _fill_celle(self, parent_iid, res):
"""Populate duplicated-cell nodes under an aisle node."""
rows = _json_obj(res).get("rows", [])
if not rows:
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "")); return
for r in rows:
idc = r["IDCella"]; ubi = r["Ubicazione"]; corsia = r.get("Corsia"); num = r.get("NumUDC", 0)
node_id = f"cella:{idc}"; label = f"{ubi} [x{num}]"
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", ""))
return
for row in rows:
idc = row["IDCella"]
ubi = row["Ubicazione"]
corsia = row.get("Corsia")
num = row.get("NumUDC", 0)
node_id = f"cella:{idc}"
label = f"{ubi} [x{num}]"
if self.tree.exists(node_id):
self.tree.item(node_id, text=label, values=(f"IDCella {idc}", ""))
else:
self.tree.insert(parent_iid, "end", iid=node_id, text=label,
values=(f"IDCella {idc}", ""), open=False, tags=("cella", f"corsia:{corsia}"))
if not any(ch.endswith("::lazy") for ch in self.tree.get_children(node_id)):
self.tree.insert(parent_iid, "end", iid=node_id, text=label, values=(f"IDCella {idc}", ""), open=False, tags=("cella", f"corsia:{corsia}"))
if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)):
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", ""))
def _load_pallet_for_cella(self, parent_iid, idcella: int):
async def _q(db): return await db.query_json(SQL_PALLET_IN_CELLA, params={"idcella": idcella}, as_dict_rows=True)
self.runner.run(_q(self.db), lambda res: self._fill_pallet(parent_iid, res),
lambda e: messagebox.showerror("Errore", str(e), parent=self))
"""Query pallet details for a duplicated cell."""
async def _q(db):
return await db.query_json(SQL_PALLET_IN_CELLA, params={"idcella": idcella}, as_dict_rows=True)
self.runner.run(_q(self.db), lambda res: self._fill_pallet(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self))
def _fill_pallet(self, parent_iid, res):
"""Add pallet leaves under the selected cell node."""
rows = _json_obj(res).get("rows", [])
if not rows:
self.tree.insert(parent_iid, "end", text="(nessun pallet)", values=("", "")); return
self.tree.insert(parent_iid, "end", text="(nessun pallet)", values=("", ""))
return
parent_tags = self.tree.item(parent_iid, "tags") or ()
corsia_tag = next((t for t in parent_tags if t.startswith("corsia:")), None)
corsia_val = corsia_tag.split(":",1)[1] if corsia_tag else ""
corsia_tag = next((tag for tag in parent_tags if tag.startswith("corsia:")), None)
corsia_val = corsia_tag.split(":", 1)[1] if corsia_tag else ""
cella_ubi = self.tree.item(parent_iid, "text")
idcella_txt = self.tree.item(parent_iid, "values")[0]
idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None
for r in rows:
pallet = r.get("Pallet", ""); desc = r.get("Descrizione", ""); lotto = r.get("Lotto", "")
for row in rows:
pallet = row.get("Pallet", "")
desc = row.get("Descrizione", "")
lotto = row.get("Lotto", "")
leaf_id = f"pallet:{idcella_num}:{pallet}"
if self.tree.exists(leaf_id):
self.tree.item(leaf_id, text=str(pallet), values=(desc, lotto)); continue
self.tree.insert(parent_iid, "end", iid=leaf_id, text=str(pallet),
values=(desc, lotto),
tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"))
self.tree.item(leaf_id, text=str(pallet), values=(desc, lotto))
continue
self.tree.insert(
parent_iid,
"end",
iid=leaf_id,
text=str(pallet),
values=(desc, lotto),
tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"),
)
def _load_riepilogo(self):
async def _q(db): return await db.query_json(SQL_RIEPILOGO_PERCENTUALI, as_dict_rows=True)
"""Load the percentage summary by aisle."""
async def _q(db):
return await db.query_json(SQL_RIEPILOGO_PERCENTUALI, as_dict_rows=True)
self.runner.run(_q(self.db), self._fill_riepilogo, lambda e: messagebox.showerror("Errore", str(e), parent=self))
def _fill_riepilogo(self, res):
"""Refresh the bottom summary table."""
rows = _json_obj(res).get("rows", [])
for i in self.sum_tbl.get_children(): self.sum_tbl.delete(i)
for r in rows:
self.sum_tbl.insert("", "end", values=(r.get("Corsia"), r.get("TotCelle",0),
r.get("CelleMultiple",0), f"{r.get('Percentuale',0):.2f}"))
for item in self.sum_tbl.get_children():
self.sum_tbl.delete(item)
for row in rows:
self.sum_tbl.insert(
"",
"end",
values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"),
)
def expand_all(self):
"""Expand all aisle roots and trigger lazy loading where needed."""
for iid in self.tree.get_children(""):
self.tree.item(iid, open=True)
if f"{iid}::lazy" in self.tree.get_children(iid):
self.tree.delete(f"{iid}::lazy")
corsia = iid.split(":",1)[1]
corsia = iid.split(":", 1)[1]
self._load_celle_for_corsia(iid, corsia)
def collapse_all(self):
"""Collapse all root nodes in the duplication tree."""
for iid in self.tree.get_children(""):
self.tree.item(iid, open=False)
def export_to_xlsx(self):
"""Export both the detailed tree and the summary table to Excel."""
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
default_name = f"esportazione_celle_udc_multiple_{ts}.xlsx"
fname = filedialog.asksaveasfilename(parent=self, title="Esporta in Excel",
defaultextension=".xlsx",
filetypes=[("Excel Workbook","*.xlsx")],
initialfile=default_name)
if not fname: return
fname = filedialog.asksaveasfilename(
parent=self,
title="Esporta in Excel",
defaultextension=".xlsx",
filetypes=[("Excel Workbook", "*.xlsx")],
initialfile=default_name,
)
if not fname:
return
try:
wb = Workbook()
ws_det = wb.active; ws_det.title = "Dettaglio"
ws_det = wb.active
ws_det.title = "Dettaglio"
ws_sum = wb.create_sheet("Riepilogo")
det_headers = ["Corsia", "Ubicazione", "IDCella", "Pallet", "Descrizione", "Lotto"]
sum_headers = ["Corsia", "TotCelle", "CelleMultiple", "Percentuale"]
def _hdr(ws, headers):
for j,h in enumerate(headers, start=1):
cell = ws.cell(row=1, column=j, value=h)
cell.font = Font(bold=True); cell.alignment = Alignment(horizontal="center", vertical="center")
_hdr(ws_det, det_headers); _hdr(ws_sum, sum_headers)
r = 2
def _hdr(ws, headers):
"""Write formatted headers into the given worksheet."""
for j, header in enumerate(headers, start=1):
cell = ws.cell(row=1, column=j, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center")
_hdr(ws_det, det_headers)
_hdr(ws_sum, sum_headers)
row_idx = 2
for corsia_node in self.tree.get_children(""):
for cella_node in self.tree.get_children(corsia_node):
for pallet_node in self.tree.get_children(cella_node):
tags = self.tree.item(pallet_node, "tags") or ()
if "pallet" not in tags: continue
corsia = next((t.split(":",1)[1] for t in tags if t.startswith("corsia:")), "")
ubi = next((t.split(":",1)[1] for t in tags if t.startswith("ubicazione:")), "")
idcella = next((t.split(":",1)[1] for t in tags if t.startswith("idcella:")), "")
if "pallet" not in tags:
continue
corsia = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("corsia:")), "")
ubi = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("ubicazione:")), "")
idcella = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("idcella:")), "")
pallet = self.tree.item(pallet_node, "text")
desc, lotto = self.tree.item(pallet_node, "values")
for j,v in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1):
ws_det.cell(row=r, column=j, value=v)
r += 1
for j, value in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1):
ws_det.cell(row=row_idx, column=j, value=value)
row_idx += 1
r2 = 2
row_idx = 2
for iid in self.sum_tbl.get_children(""):
vals = self.sum_tbl.item(iid, "values")
for j, v in enumerate(vals, start=1):
ws_sum.cell(row=r2, column=j, value=v)
r2 += 1
for j, value in enumerate(vals, start=1):
ws_sum.cell(row=row_idx, column=j, value=value)
row_idx += 1
def _autosize(ws):
"""Resize worksheet columns based on their longest value."""
widths = {}
for row in ws.iter_rows(values_only=True):
for j, val in enumerate(row, start=1):
val_s = "" if val is None else str(val)
widths[j] = max(widths.get(j, 0), len(val_s))
for j, value in enumerate(row, start=1):
value_s = "" if value is None else str(value)
widths[j] = max(widths.get(j, 0), len(value_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)
_autosize(ws_det); _autosize(ws_sum)
wb.save(fname); messagebox.showinfo("Esportazione completata", f"File creato:\n{fname}", parent=self)
for j, width in widths.items():
ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60)
_autosize(ws_det)
_autosize(ws_sum)
wb.save(fname)
messagebox.showinfo("Esportazione completata", f"File creato:\n{fname}", parent=self)
except Exception as ex:
messagebox.showerror("Errore esportazione", str(ex), parent=self)
def open_celle_multiple_window(root: tk.Tk, db_client, runner: AsyncRunner | None = None):
win = CelleMultipleWindow(root, db_client, runner=runner); win.lift(); win.focus_set(); return win
"""Create, focus and return the duplicated-cells explorer."""
win = CelleMultipleWindow(root, db_client, runner=runner)
win.lift()
win.focus_set()
return win