Files
ware_house/gestione_scarico.py

773 lines
25 KiB
Python

"""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 AsyncRunner
from audit_log import log_user_action
from busy_overlay import InlineBusyOverlay
from locale_text import load_locale_catalog, text as loc_text
from ui_theme import theme_color, theme_font, theme_section, theme_value
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 = """
SET NOCOUNT ON;
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.py_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, commit=True)
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._theme = theme_section("scarico_dialog", {})
self._locale_catalog = load_locale_catalog()
self._busy = InlineBusyOverlay(self, self._theme)
self._async = AsyncRunner(self)
self.rows_tree: ttk.Treeview | None = None
self.title(loc_text("scarico.title", catalog=self._locale_catalog, default="Scarica {ubicazione}").format(ubicazione=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=loc_text("scarico.label.location", catalog=self._locale_catalog, default="Ubicazione: {ubicazione}").format(ubicazione=self.ubicazione),
font=theme_font(self._theme, "header_font", ("Segoe UI", 13, "bold")),
).grid(
row=0, column=0, sticky="w"
)
ctk.CTkLabel(
top,
text=loc_text("scarico.label.select", catalog=self._locale_catalog, default="Seleziona le UDC da scaricare"),
anchor="w",
font=theme_font(self._theme, "body_font", ("Segoe UI", 10)),
).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=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)))
style.configure("Scarico.Treeview.Heading", font=theme_font(self._theme, "tree_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=loc_text("scarico.col.udc", catalog=self._locale_catalog, default="UDC"))
self.rows_tree.heading("last", text=loc_text("scarico.col.last_insert", catalog=self._locale_catalog, default="Ultimo inserimento"))
self.rows_tree.heading("diag", text=loc_text("scarico.col.diagnostic", catalog=self._locale_catalog, default="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=loc_text("scarico.button.submit", catalog=self._locale_catalog, default="Scarica"),
command=self._on_scarica,
font=theme_font(self._theme, "button_font", ("Segoe UI", 10, "bold")),
).grid(
row=0, column=1, padx=(8, 0), pady=8
)
ctk.CTkButton(
actions,
text=loc_text("scarico.button.close", catalog=self._locale_catalog, default="Chiudi"),
command=self._close,
font=theme_font(self._theme, "button_font", ("Segoe UI", 10, "bold")),
).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(
loc_text("scarico.msg.title", catalog=self._locale_catalog, default="Scarica"),
loc_text("scarico.msg.load_error", catalog=self._locale_catalog, default="Caricamento UDC fallito:\n{error}").format(error=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(
loc_text("scarico.msg.title", catalog=self._locale_catalog, default="Scarica"),
loc_text("scarico.msg.select_one", catalog=self._locale_catalog, default="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(
loc_text("scarico.msg.title", catalog=self._locale_catalog, default="Scarica"),
loc_text("scarico.msg.exec_error", catalog=self._locale_catalog, default="Scarico fallito:\n{error}").format(error=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,
)