Checkpoint before ghost pallet cleanup workflow
This commit is contained in:
734
gestione_scarico.py
Normal file
734
gestione_scarico.py
Normal file
@@ -0,0 +1,734 @@
|
||||
"""Modal dialog used to unload one or more UDCs from a multi-occupancy cell."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from gestione_aree import BusyOverlay, AsyncRunner
|
||||
from audit_log import log_user_action
|
||||
from user_session import UserSession
|
||||
|
||||
try:
|
||||
from loguru import logger
|
||||
except Exception: # pragma: no cover - fallback used only when Loguru is not available
|
||||
class _FallbackLogger:
|
||||
"""Minimal adapter used only when Loguru is not installed yet."""
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__)
|
||||
self._logger.setLevel(logging.DEBUG)
|
||||
self._logger.propagate = False
|
||||
|
||||
def bind(self, **_kwargs):
|
||||
return self
|
||||
|
||||
def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs):
|
||||
handler: logging.Handler
|
||||
if hasattr(sink, "write"):
|
||||
handler = logging.StreamHandler(sink)
|
||||
else:
|
||||
handler = logging.FileHandler(str(sink), encoding=encoding)
|
||||
handler.setLevel(getattr(logging, str(level).upper(), logging.INFO))
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
||||
)
|
||||
self._logger.addHandler(handler)
|
||||
return 0
|
||||
|
||||
def log(self, level, message):
|
||||
getattr(self._logger, str(level).lower(), self._logger.info)(message)
|
||||
|
||||
def debug(self, message):
|
||||
self._logger.debug(message)
|
||||
|
||||
def info(self, message):
|
||||
self._logger.info(message)
|
||||
|
||||
def exception(self, message):
|
||||
self._logger.exception(message)
|
||||
|
||||
logger = _FallbackLogger()
|
||||
|
||||
|
||||
SCARICO_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG"
|
||||
MODULE_LOG_NAME = Path(__file__).stem
|
||||
MODULE_LOG_PATH = Path(__file__).with_suffix(".log")
|
||||
_MODULE_LOG_ENABLED = SCARICO_LOG_MODE.upper() != "OFF"
|
||||
_MODULE_LOG_LEVEL = "DEBUG" if SCARICO_LOG_MODE.upper() == "DEBUG" else "INFO"
|
||||
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
|
||||
_MODULE_LOGGING_CONFIGURED = False
|
||||
DEFAULT_SCARICO_USER = "warehouse_ui"
|
||||
|
||||
|
||||
def _session_login(session: UserSession | None, fallback: str | None = None) -> str:
|
||||
"""Return the current application login, falling back to a technical user."""
|
||||
|
||||
if session and str(session.login or "").strip():
|
||||
return str(session.login).strip()
|
||||
return str((fallback or DEFAULT_SCARICO_USER) or DEFAULT_SCARICO_USER).strip()
|
||||
|
||||
|
||||
def _configure_module_logger():
|
||||
"""Configure console and file logging for this module."""
|
||||
global _MODULE_LOGGING_CONFIGURED
|
||||
if _MODULE_LOGGING_CONFIGURED:
|
||||
return
|
||||
if not _MODULE_LOG_ENABLED:
|
||||
_MODULE_LOGGING_CONFIGURED = True
|
||||
return
|
||||
|
||||
record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level=_MODULE_LOG_LEVEL,
|
||||
colorize=True,
|
||||
filter=record_filter,
|
||||
format=(
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>" + MODULE_LOG_NAME + "</cyan> | "
|
||||
"<level>{message}</level>"
|
||||
),
|
||||
)
|
||||
logger.add(
|
||||
MODULE_LOG_PATH,
|
||||
level=_MODULE_LOG_LEVEL,
|
||||
colorize=False,
|
||||
encoding="utf-8",
|
||||
filter=record_filter,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}",
|
||||
)
|
||||
_MODULE_LOGGING_CONFIGURED = True
|
||||
|
||||
|
||||
def _format_payload(payload: Any) -> str:
|
||||
"""Serialize payloads for human-readable logging."""
|
||||
try:
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
except Exception:
|
||||
return repr(payload)
|
||||
|
||||
|
||||
def _log_call(level: str | None = None):
|
||||
"""Trace entry, exit and failure of selected high-level functions."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
effective_level = level or _MODULE_LOG_LEVEL
|
||||
_MODULE_LOGGER.log(
|
||||
effective_level,
|
||||
f"CALL {func.__qualname__} args={_format_payload(args[1:] if args else ())} kwargs={_format_payload(kwargs)}",
|
||||
)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except Exception:
|
||||
_MODULE_LOGGER.exception(f"FAIL {func.__qualname__}")
|
||||
raise
|
||||
_MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}")
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _log_sql(query_name: str, sql: str, params: dict[str, Any]):
|
||||
"""Log one SQL statement and its parameters."""
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params)}")
|
||||
_MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}")
|
||||
|
||||
|
||||
def _log_dataset(query_name: str, rows: list[Any]):
|
||||
"""Log query results at summary or full-debug level depending on the flag."""
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows")
|
||||
if SCARICO_LOG_MODE.upper() == "DEBUG":
|
||||
_MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}")
|
||||
|
||||
|
||||
_configure_module_logger()
|
||||
if _MODULE_LOG_ENABLED:
|
||||
_MODULE_LOGGER.info(
|
||||
f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={SCARICO_LOG_MODE.upper()}"
|
||||
)
|
||||
|
||||
|
||||
SQL_UDC_IN_CELLA = """
|
||||
WITH cell_pallets AS (
|
||||
SELECT DISTINCT
|
||||
g.BarcodePallet
|
||||
FROM dbo.XMag_GiacenzaPallet g
|
||||
WHERE g.IDCella = :idcella
|
||||
),
|
||||
last_in_cell AS (
|
||||
SELECT
|
||||
mp.Attributo AS BarcodePallet,
|
||||
MAX(mp.ID) AS LastID
|
||||
FROM dbo.MagazziniPallet mp
|
||||
JOIN cell_pallets cp
|
||||
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||
WHERE mp.IDCella = :idcella
|
||||
AND mp.Tipo = 'V'
|
||||
GROUP BY mp.Attributo
|
||||
),
|
||||
last_move AS (
|
||||
SELECT
|
||||
mp.Attributo AS BarcodePallet,
|
||||
mp.ID,
|
||||
mp.DataMagazzino
|
||||
FROM dbo.MagazziniPallet mp
|
||||
JOIN last_in_cell lic ON lic.LastID = mp.ID
|
||||
),
|
||||
latest_any AS (
|
||||
SELECT
|
||||
ranked.BarcodePallet,
|
||||
ranked.IDCella
|
||||
FROM (
|
||||
SELECT
|
||||
mp.Attributo AS BarcodePallet,
|
||||
mp.IDCella,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY mp.Attributo
|
||||
ORDER BY mp.ID DESC
|
||||
) AS rn
|
||||
FROM dbo.MagazziniPallet mp
|
||||
JOIN cell_pallets cp
|
||||
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||
WHERE mp.Tipo = 'V'
|
||||
AND mp.PesoUnitario > 0
|
||||
) ranked
|
||||
WHERE ranked.rn = 1
|
||||
),
|
||||
shipped AS (
|
||||
SELECT DISTINCT
|
||||
shipped.BarcodePallet
|
||||
FROM dbo.XMag_GiacenzaPalletPlistChiuse shipped
|
||||
JOIN cell_pallets cp
|
||||
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
shipped.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
)
|
||||
SELECT
|
||||
cp.BarcodePallet AS UDC,
|
||||
lm.ID AS SourceID,
|
||||
lm.DataMagazzino AS LastEventAt,
|
||||
CASE
|
||||
WHEN shipped.BarcodePallet IS NOT NULL THEN CAST(1 AS int)
|
||||
ELSE CAST(0 AS int)
|
||||
END AS IsShippedGhost,
|
||||
CASE
|
||||
WHEN la.IDCella IS NOT NULL
|
||||
AND la.IDCella <> :idcella
|
||||
THEN CAST(1 AS int)
|
||||
ELSE CAST(0 AS int)
|
||||
END AS IsMovedGhost
|
||||
FROM cell_pallets cp
|
||||
LEFT JOIN last_move lm
|
||||
ON lm.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
LEFT JOIN latest_any la
|
||||
ON la.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
LEFT JOIN shipped
|
||||
ON shipped.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||
ORDER BY
|
||||
lm.ID DESC,
|
||||
cp.BarcodePallet DESC;
|
||||
"""
|
||||
|
||||
|
||||
SQL_SCARICA_UDC = """
|
||||
DECLARE @Now datetime = GETDATE();
|
||||
DECLARE @SourceID int = 0;
|
||||
DECLARE @NumeroPallet int = 0;
|
||||
DECLARE @PesoUnitario float = 1;
|
||||
DECLARE @Tara float = 0;
|
||||
DECLARE @SourceIDCella int = 0;
|
||||
|
||||
SELECT TOP (1)
|
||||
@SourceID = src.ID,
|
||||
@NumeroPallet = ISNULL(src.NumeroPallet, 0),
|
||||
@PesoUnitario = ISNULL(NULLIF(src.PesoUnitario, 0), 1),
|
||||
@Tara = ISNULL(src.Tara, 0),
|
||||
@SourceIDCella = ISNULL(src.IDCella, 0)
|
||||
FROM dbo.MagazziniPallet src
|
||||
WHERE src.Attributo = :barcode_pallet
|
||||
AND src.Tipo = 'V'
|
||||
AND src.PesoUnitario > 0
|
||||
ORDER BY src.ID DESC;
|
||||
|
||||
IF @SourceID > 0
|
||||
BEGIN
|
||||
UPDATE dbo.MagazziniPallet
|
||||
SET ModUtente = :utente,
|
||||
ModDataOra = @Now
|
||||
WHERE ID = @SourceID;
|
||||
|
||||
INSERT INTO dbo.MagazziniPallet (
|
||||
Tipo,
|
||||
IDRiferimento,
|
||||
NumeroPallet,
|
||||
Attributo,
|
||||
IDMagazzino,
|
||||
IDArea,
|
||||
IDCella,
|
||||
DataMagazzino,
|
||||
PesoUnitario,
|
||||
Tara,
|
||||
InsUtente,
|
||||
InsDataOra
|
||||
)
|
||||
SELECT
|
||||
'P',
|
||||
@SourceID,
|
||||
ISNULL(src.NumeroPallet, 0),
|
||||
src.Attributo,
|
||||
src.IDMagazzino,
|
||||
src.IDArea,
|
||||
src.IDCella,
|
||||
@Now,
|
||||
ISNULL(NULLIF(src.PesoUnitario, 0), 1),
|
||||
ISNULL(src.Tara, 0),
|
||||
:utente,
|
||||
@Now
|
||||
FROM dbo.MagazziniPallet src
|
||||
WHERE src.ID = @SourceID;
|
||||
|
||||
UPDATE dbo.Celle
|
||||
SET IDStato = 0,
|
||||
ModUtente = :utente,
|
||||
ModDataOra = @Now
|
||||
WHERE ID = @SourceIDCella;
|
||||
END;
|
||||
|
||||
INSERT INTO dbo.MagazziniPallet (
|
||||
Tipo,
|
||||
IDRiferimento,
|
||||
NumeroPallet,
|
||||
Attributo,
|
||||
IDMagazzino,
|
||||
IDArea,
|
||||
IDCella,
|
||||
DataMagazzino,
|
||||
PesoUnitario,
|
||||
Tara,
|
||||
InsUtente,
|
||||
InsDataOra
|
||||
)
|
||||
SELECT
|
||||
'V',
|
||||
@SourceID,
|
||||
@NumeroPallet,
|
||||
:barcode_pallet,
|
||||
target.IDMagazzino,
|
||||
target.IDArea,
|
||||
target.ID,
|
||||
@Now,
|
||||
@PesoUnitario,
|
||||
@Tara,
|
||||
:utente,
|
||||
@Now
|
||||
FROM (
|
||||
SELECT c.ID, c.IDArea, a.IDMagazzino
|
||||
FROM dbo.Celle c
|
||||
JOIN dbo.Aree a ON a.ID = c.IDArea
|
||||
WHERE c.ID = :target_idcella
|
||||
) AS target;
|
||||
|
||||
EXEC dbo.sp_ControllaPrenotazionePackingListPalletNew;
|
||||
|
||||
SELECT
|
||||
CAST(1 AS int) AS Ok,
|
||||
@SourceID AS SourceID,
|
||||
:target_idcella AS TargetIDCella,
|
||||
:target_barcode_cella AS TargetBarcodeCella;
|
||||
"""
|
||||
|
||||
|
||||
async def move_pallet_async(
|
||||
db_client,
|
||||
*,
|
||||
barcode_pallet: str,
|
||||
target_idcella: int,
|
||||
target_barcode_cella: str,
|
||||
utente: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Move one pallet to a target cell using the same movement semantics as the legacy app.
|
||||
|
||||
The original C# application delegates load, transfer and unload to
|
||||
``sp_xMagGestioneMagazziniPallet``. The Python app mirrors the same
|
||||
behavior with an explicit SQL batch that:
|
||||
1. finds the latest positive row for the pallet,
|
||||
2. registers a compensating ``P`` move on the source cell,
|
||||
3. frees the previous cell reservation,
|
||||
4. inserts a new ``V`` move on the target cell,
|
||||
5. re-runs the packing-list reservation check.
|
||||
"""
|
||||
params = {
|
||||
"barcode_pallet": str(barcode_pallet or "").strip(),
|
||||
"target_idcella": int(target_idcella),
|
||||
"target_barcode_cella": str(target_barcode_cella or "").strip(),
|
||||
"utente": str((utente or DEFAULT_SCARICO_USER) or "warehouse_ui").strip(),
|
||||
}
|
||||
_log_sql("move_pallet", SQL_SCARICA_UDC, params)
|
||||
res = await db_client.query_json(SQL_SCARICA_UDC, params)
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
_log_dataset("move_pallet", rows)
|
||||
first = rows[0] if rows else [1, 0, params["target_idcella"], params["target_barcode_cella"]]
|
||||
return {
|
||||
"ok": int(first[0] or 0),
|
||||
"source_id": int(first[1] or 0),
|
||||
"target_idcella": int(first[2] or 0),
|
||||
"target_barcode_cella": str(first[3] or ""),
|
||||
"barcode_pallet": params["barcode_pallet"],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScaricoRow:
|
||||
"""View-model describing one unloadable UDC currently present in a cell."""
|
||||
|
||||
udc: str
|
||||
source_id: int | None
|
||||
last_event_at: str
|
||||
diagnostic_note: str
|
||||
selected: tk.BooleanVar
|
||||
|
||||
|
||||
def _build_diagnostic_note(is_shipped: int | bool, is_moved: int | bool) -> str:
|
||||
"""Translate low-level anomaly flags into one operator-facing note."""
|
||||
|
||||
notes: list[str] = []
|
||||
if bool(is_shipped):
|
||||
notes.append("Mancato scarico: spedita")
|
||||
if bool(is_moved):
|
||||
notes.append("Mancato scarico: spostata")
|
||||
return " | ".join(notes)
|
||||
|
||||
|
||||
class ScaricoDialog(ctk.CTkToplevel):
|
||||
"""Modal dialog that allows unloading selected UDCs from one cell."""
|
||||
|
||||
CHECKBOX_COL_W = 56
|
||||
UDC_COL_W = 130
|
||||
DATE_COL_W = 180
|
||||
DIAG_COL_W = 320
|
||||
|
||||
@_log_call()
|
||||
def __init__(
|
||||
self,
|
||||
parent: tk.Misc,
|
||||
*,
|
||||
db_client,
|
||||
idcella: int,
|
||||
ubicazione: str,
|
||||
on_completed: Callable[[], None] | None = None,
|
||||
session: UserSession | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.db_client = db_client
|
||||
self.idcella = idcella
|
||||
self.ubicazione = ubicazione
|
||||
self.on_completed = on_completed
|
||||
self.session = session
|
||||
self.rows: list[ScaricoRow] = []
|
||||
self._busy = BusyOverlay(self)
|
||||
self._async = AsyncRunner(self)
|
||||
self.rows_tree: ttk.Treeview | None = None
|
||||
|
||||
self.title(f"Scarica {ubicazione}")
|
||||
self.resizable(False, False)
|
||||
self.transient(parent)
|
||||
self.protocol("WM_DELETE_WINDOW", self._close)
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
|
||||
self._build_ui()
|
||||
self._load_rows()
|
||||
|
||||
self.update_idletasks()
|
||||
self.grab_set()
|
||||
try:
|
||||
self.wait_visibility()
|
||||
except Exception:
|
||||
pass
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
|
||||
def _build_ui(self):
|
||||
"""Build the compact modal layout."""
|
||||
top = ctk.CTkFrame(self)
|
||||
top.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 6))
|
||||
top.grid_columnconfigure(0, weight=1)
|
||||
ctk.CTkLabel(top, text=f"Ubicazione: {self.ubicazione}", font=("", 13, "bold")).grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
ctk.CTkLabel(top, text="Seleziona le UDC da scaricare", anchor="w").grid(
|
||||
row=1, column=0, sticky="w", pady=(4, 0)
|
||||
)
|
||||
|
||||
table = ctk.CTkFrame(self)
|
||||
table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 8))
|
||||
table.grid_rowconfigure(0, weight=1)
|
||||
table.grid_columnconfigure(0, weight=1)
|
||||
|
||||
tree_host = tk.Frame(table, bd=0, highlightthickness=0, background="#DBDBDB")
|
||||
tree_host.grid(row=0, column=0, sticky="nsew", padx=8, pady=(8, 8))
|
||||
tree_host.grid_rowconfigure(0, weight=1)
|
||||
tree_host.grid_columnconfigure(0, weight=1)
|
||||
|
||||
style = ttk.Style(self)
|
||||
style.configure("Scarico.Treeview", rowheight=28, font=("Segoe UI", 10))
|
||||
style.configure("Scarico.Treeview.Heading", font=("Segoe UI", 10, "bold"))
|
||||
|
||||
self.rows_tree = ttk.Treeview(
|
||||
tree_host,
|
||||
columns=("sel", "udc", "last", "diag"),
|
||||
show="headings",
|
||||
style="Scarico.Treeview",
|
||||
selectmode="none",
|
||||
)
|
||||
self.rows_tree.heading("sel", text="Sel")
|
||||
self.rows_tree.heading("udc", text="UDC")
|
||||
self.rows_tree.heading("last", text="Ultimo inserimento")
|
||||
self.rows_tree.heading("diag", text="Diagnostica")
|
||||
self.rows_tree.column("sel", width=self.CHECKBOX_COL_W, stretch=False, anchor="center")
|
||||
self.rows_tree.column("udc", width=self.UDC_COL_W, stretch=False, anchor="w")
|
||||
self.rows_tree.column("last", width=self.DATE_COL_W, stretch=False, anchor="w")
|
||||
self.rows_tree.column("diag", width=self.DIAG_COL_W, stretch=True, anchor="w")
|
||||
self.rows_tree.grid(row=0, column=0, sticky="nsew")
|
||||
self.rows_tree.bind("<Button-1>", self._on_tree_click, add="+")
|
||||
|
||||
tree_scroll = ttk.Scrollbar(tree_host, orient="vertical", command=self.rows_tree.yview)
|
||||
tree_scroll.grid(row=0, column=1, sticky="ns")
|
||||
self.rows_tree.configure(yscrollcommand=tree_scroll.set)
|
||||
|
||||
actions = ctk.CTkFrame(self)
|
||||
actions.grid(row=2, column=0, sticky="ew", padx=10, pady=(0, 10))
|
||||
actions.grid_columnconfigure(0, weight=1)
|
||||
ctk.CTkButton(actions, text="scarica", command=self._on_scarica).grid(
|
||||
row=0, column=1, padx=(8, 0), pady=8
|
||||
)
|
||||
ctk.CTkButton(actions, text="close", command=self._close).grid(
|
||||
row=0, column=2, padx=(8, 8), pady=8
|
||||
)
|
||||
|
||||
def _render_rows(self):
|
||||
"""Recreate the compact list of unloadable UDC rows."""
|
||||
if self.rows_tree is None:
|
||||
return
|
||||
for item in self.rows_tree.get_children():
|
||||
self.rows_tree.delete(item)
|
||||
|
||||
for idx, row in enumerate(self.rows):
|
||||
self.rows_tree.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=str(idx),
|
||||
values=(
|
||||
"[x]" if row.selected.get() else "[ ]",
|
||||
row.udc,
|
||||
row.last_event_at,
|
||||
row.diagnostic_note or "",
|
||||
),
|
||||
)
|
||||
|
||||
self.update_idletasks()
|
||||
width = max(900, min(1040, self.winfo_reqwidth() + 20))
|
||||
total_height = max(210, min(460, self.winfo_reqheight() + 8))
|
||||
self.geometry(f"{width}x{total_height}")
|
||||
|
||||
def _on_tree_click(self, event):
|
||||
"""Toggle the pseudo-checkbox when the operator clicks the first column."""
|
||||
|
||||
if self.rows_tree is None:
|
||||
return
|
||||
region = self.rows_tree.identify("region", event.x, event.y)
|
||||
if region != "cell":
|
||||
return
|
||||
column = self.rows_tree.identify_column(event.x)
|
||||
item_id = self.rows_tree.identify_row(event.y)
|
||||
if column != "#1" or not item_id:
|
||||
return
|
||||
try:
|
||||
row = self.rows[int(item_id)]
|
||||
except Exception:
|
||||
return
|
||||
row.selected.set(not row.selected.get())
|
||||
self.rows_tree.set(item_id, "sel", "[x]" if row.selected.get() else "[ ]")
|
||||
return "break"
|
||||
|
||||
@_log_call()
|
||||
def _load_rows(self):
|
||||
"""Load the list of current UDCs ordered from newest to oldest."""
|
||||
params = {"idcella": self.idcella}
|
||||
_log_sql("scarico_load_rows", SQL_UDC_IN_CELLA, params)
|
||||
|
||||
def _ok(res):
|
||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||
_log_dataset("scarico_load_rows", rows)
|
||||
self.rows = []
|
||||
for udc, source_id, last_event_at, is_shipped, is_moved in rows:
|
||||
if isinstance(last_event_at, datetime):
|
||||
last_event = last_event_at.strftime("%d/%m/%Y %H:%M:%S")
|
||||
else:
|
||||
last_event = str(last_event_at or "")
|
||||
self.rows.append(
|
||||
ScaricoRow(
|
||||
udc=str(udc or ""),
|
||||
source_id=int(source_id) if source_id is not None else None,
|
||||
last_event_at=last_event,
|
||||
diagnostic_note=_build_diagnostic_note(is_shipped, is_moved),
|
||||
selected=tk.BooleanVar(value=False),
|
||||
)
|
||||
)
|
||||
self._render_rows()
|
||||
self._busy.hide()
|
||||
|
||||
def _err(ex):
|
||||
self._busy.hide()
|
||||
_MODULE_LOGGER.exception(f"Errore caricamento righe scarico idcella={self.idcella}: {ex}")
|
||||
messagebox.showerror("Scarica", f"Caricamento UDC fallito:\n{ex}", parent=self)
|
||||
self._close()
|
||||
|
||||
self._async.run(
|
||||
self.db_client.query_json(SQL_UDC_IN_CELLA, params),
|
||||
_ok,
|
||||
_err,
|
||||
busy=self._busy,
|
||||
message="Carico UDC...",
|
||||
)
|
||||
|
||||
@_log_call()
|
||||
def _on_scarica(self):
|
||||
"""Unload the UDCs selected by the user from the current cell."""
|
||||
selected = [row for row in self.rows if row.selected.get()]
|
||||
if not selected:
|
||||
messagebox.showinfo("Scarica", "Seleziona almeno una UDC da scaricare.", parent=self)
|
||||
return
|
||||
|
||||
if not messagebox.askyesno(
|
||||
"Conferma scarico",
|
||||
f"Scaricare {len(selected)} UDC da {self.ubicazione}?",
|
||||
parent=self,
|
||||
):
|
||||
return
|
||||
|
||||
async def _job():
|
||||
results: list[dict[str, Any]] = []
|
||||
for row in selected:
|
||||
result = await move_pallet_async(
|
||||
self.db_client,
|
||||
barcode_pallet=row.udc,
|
||||
target_idcella=9999,
|
||||
target_barcode_cella="9000000",
|
||||
utente=_session_login(self.session),
|
||||
)
|
||||
results.append({"udc": row.udc, "affected": int(result.get("ok") or 0)})
|
||||
return results
|
||||
|
||||
def _ok(results):
|
||||
_log_dataset("scarica_udc", results)
|
||||
done = [item["udc"] for item in results if int(item.get("affected") or 0) > 0]
|
||||
skipped = [item["udc"] for item in results if int(item.get("affected") or 0) <= 0]
|
||||
if not done:
|
||||
messagebox.showwarning(
|
||||
"Scarica",
|
||||
"Nessuna UDC e' stata scaricata. Verifica che le unita' siano ancora presenti in cella.",
|
||||
parent=self,
|
||||
)
|
||||
return
|
||||
if skipped:
|
||||
messagebox.showwarning(
|
||||
"Scarica",
|
||||
"Scarico parziale.\nCompletate: "
|
||||
+ ", ".join(done)
|
||||
+ "\nNon scaricate: "
|
||||
+ ", ".join(skipped),
|
||||
parent=self,
|
||||
)
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Scarica",
|
||||
"Scarico completato per:\n" + "\n".join(done),
|
||||
parent=self,
|
||||
)
|
||||
log_user_action(
|
||||
self.session,
|
||||
module=MODULE_LOG_NAME,
|
||||
action="layout.scarico",
|
||||
outcome="ok",
|
||||
target=self.ubicazione,
|
||||
details={"scaricate": done, "saltate": skipped},
|
||||
)
|
||||
if self.on_completed:
|
||||
self.on_completed()
|
||||
self._close()
|
||||
|
||||
def _err(ex):
|
||||
_MODULE_LOGGER.exception(f"Errore scarico idcella={self.idcella}: {ex}")
|
||||
log_user_action(
|
||||
self.session,
|
||||
module=MODULE_LOG_NAME,
|
||||
action="layout.scarico",
|
||||
outcome="error",
|
||||
target=self.ubicazione,
|
||||
details={"error": str(ex)},
|
||||
)
|
||||
messagebox.showerror("Scarica", f"Scarico fallito:\n{ex}", parent=self)
|
||||
|
||||
self._async.run(
|
||||
_job(),
|
||||
_ok,
|
||||
_err,
|
||||
busy=self._busy,
|
||||
message="Scarico UDC...",
|
||||
)
|
||||
|
||||
@_log_call()
|
||||
def _close(self):
|
||||
"""Release the modal grab and close the dialog safely."""
|
||||
try:
|
||||
self.grab_release()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@_log_call()
|
||||
def open_scarico_dialog(
|
||||
parent: tk.Misc,
|
||||
*,
|
||||
db_client,
|
||||
idcella: int,
|
||||
ubicazione: str,
|
||||
on_completed: Callable[[], None] | None = None,
|
||||
session: UserSession | None = None,
|
||||
) -> ScaricoDialog:
|
||||
"""Create and return the modal unload dialog for one multi-UDC cell."""
|
||||
return ScaricoDialog(
|
||||
parent,
|
||||
db_client=db_client,
|
||||
idcella=idcella,
|
||||
ubicazione=ubicazione,
|
||||
on_completed=on_completed,
|
||||
session=session,
|
||||
)
|
||||
Reference in New Issue
Block a user