Aggiungi sequenza prelievo e gestisci destinazioni senza nazione
This commit is contained in:
584
pickinglist_sequence.py
Normal file
584
pickinglist_sequence.py
Normal file
@@ -0,0 +1,584 @@
|
||||
"""Editor for the per-document picking-list pickup sequence.
|
||||
|
||||
The barcode client remains unaware of the editor: it reads the effective order
|
||||
from ``dbo.py_ViewPackingListPrelievo``. This module only manages the Python-
|
||||
specific configuration rows used by that view.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
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
|
||||
from user_session import UserSession
|
||||
from version_info import module_version, versioned_title
|
||||
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
|
||||
|
||||
__version__ = module_version(__name__)
|
||||
|
||||
|
||||
SQL_LOAD_GROUPS = """
|
||||
SELECT
|
||||
GruppoPrelievoChiave,
|
||||
MAX(GruppoPrelievo) AS GruppoPrelievo,
|
||||
MIN(OrdineGruppoDefault) AS OrdineGruppoDefault,
|
||||
MIN(OrdineGruppoConfigurato) AS OrdineGruppoConfigurato
|
||||
FROM dbo.py_ViewPackingListPrelievo
|
||||
WHERE CAST(Documento AS varchar(50)) = :documento
|
||||
AND Cella <> 9999
|
||||
GROUP BY GruppoPrelievoChiave
|
||||
ORDER BY
|
||||
CASE WHEN MIN(OrdineGruppoConfigurato) IS NULL THEN 1 ELSE 0 END,
|
||||
MIN(OrdineGruppoConfigurato),
|
||||
MIN(OrdineGruppoDefault),
|
||||
MAX(GruppoPrelievo);
|
||||
"""
|
||||
|
||||
|
||||
SQL_RESET_SEQUENCE = """
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
UPDATE dbo.py_PickingListPrelievoSequenza
|
||||
SET DelUtente = :utente,
|
||||
DelDataOra = SYSDATETIME(),
|
||||
ModUtente = :utente,
|
||||
ModDataOra = SYSDATETIME()
|
||||
WHERE Documento = :documento
|
||||
AND DelDataOra IS NULL;
|
||||
SELECT CAST(0 AS int) AS ElementiSalvati;
|
||||
"""
|
||||
|
||||
|
||||
def _rows_to_dicts(result: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
"""Convert the shared database payload to dictionaries."""
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return []
|
||||
rows = result.get("rows") or []
|
||||
columns = result.get("columns") or []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
return [dict(row) for row in rows]
|
||||
return [
|
||||
{str(columns[index]): row[index] for index in range(min(len(columns), len(row)))}
|
||||
for row in rows
|
||||
if isinstance(row, (list, tuple))
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PickupGroup:
|
||||
"""One rack group, or the conventional non-shelved group."""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
default_order: int
|
||||
|
||||
|
||||
class PickingListSequenceWindow(ctk.CTkToplevel):
|
||||
"""Drag-and-drop editor for one picking-list pickup sequence."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: tk.Misc,
|
||||
db_client,
|
||||
*,
|
||||
documento: str,
|
||||
session: UserSession | None,
|
||||
on_saved: Callable[[], None] | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.db_client = db_client
|
||||
self.documento = str(documento or "").strip()
|
||||
self.session = session
|
||||
self.on_saved = on_saved
|
||||
self._catalog = load_locale_catalog()
|
||||
self._tooltip_catalog = load_tooltip_catalog()
|
||||
self._theme = theme_section("picking_sequence_window", {})
|
||||
self.runner = AsyncRunner(self)
|
||||
self.busy = InlineBusyOverlay(self, self._theme)
|
||||
self.available: list[PickupGroup] = []
|
||||
self.selected: list[PickupGroup] = []
|
||||
self._selected_key: str | None = None
|
||||
self._selected_side: str | None = None
|
||||
self._drag_group: PickupGroup | None = None
|
||||
self._drag_side: str | None = None
|
||||
|
||||
self.title(versioned_title(self._t("picking.sequence.title", "Sequenza di prelievo"), __name__))
|
||||
self.geometry("780x560")
|
||||
self.minsize(680, 480)
|
||||
self.transient(parent)
|
||||
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
self.bind("<Escape>", lambda _event: self.destroy())
|
||||
|
||||
self._build_layout()
|
||||
self.withdraw()
|
||||
self.after_idle(self._show_and_load)
|
||||
|
||||
def _t(self, key: str, default: str, **values: Any) -> str:
|
||||
"""Return one localized and formatted label."""
|
||||
|
||||
value = loc_text(key, catalog=self._catalog, default=default)
|
||||
try:
|
||||
return value.format(**values)
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
def _build_layout(self) -> None:
|
||||
"""Build the two lists and the explicit keyboard-friendly controls."""
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
self.grid_rowconfigure(2, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
self,
|
||||
text=self._t("picking.sequence.document", "Picking List {documento}", documento=self.documento),
|
||||
font=theme_font(self._theme, "title_font", ("Segoe UI", 16, "bold")),
|
||||
).grid(row=0, column=0, columnspan=3, padx=16, pady=(14, 4), sticky="w")
|
||||
|
||||
ctk.CTkLabel(
|
||||
self,
|
||||
text=self._t(
|
||||
"picking.sequence.hint",
|
||||
"Trascina gli scaffali a destra. Quelli non inseriti saranno accodati secondo l'ordine predefinito.",
|
||||
),
|
||||
anchor="w",
|
||||
justify="left",
|
||||
wraplength=720,
|
||||
).grid(row=1, column=0, columnspan=3, padx=16, pady=(0, 10), sticky="ew")
|
||||
|
||||
self.available_panel, self.available_list = self._build_list_panel(
|
||||
0, self._t("picking.sequence.available", "Scaffali disponibili")
|
||||
)
|
||||
controls = ctk.CTkFrame(self, fg_color="transparent")
|
||||
controls.grid(row=2, column=1, padx=10, pady=4)
|
||||
for label, command in (
|
||||
(self._t("picking.sequence.button.add", "Aggiungi >"), self._add_selected),
|
||||
(self._t("picking.sequence.button.remove", "< Rimuovi"), self._remove_selected),
|
||||
(self._t("picking.sequence.button.up", "Su"), lambda: self._move_selected(-1)),
|
||||
(self._t("picking.sequence.button.down", "Giu'"), lambda: self._move_selected(1)),
|
||||
):
|
||||
button = ctk.CTkButton(controls, text=label, width=120, command=command)
|
||||
button.pack(pady=5)
|
||||
tooltip_key = {
|
||||
self._t("picking.sequence.button.add", "Aggiungi >"): "picking.sequence.add",
|
||||
self._t("picking.sequence.button.remove", "< Rimuovi"): "picking.sequence.remove",
|
||||
self._t("picking.sequence.button.up", "Su"): "picking.sequence.up",
|
||||
self._t("picking.sequence.button.down", "Giu'"): "picking.sequence.down",
|
||||
}.get(label, "")
|
||||
self._attach_tooltip(button, tooltip_key)
|
||||
|
||||
self.selected_panel, self.selected_list = self._build_list_panel(
|
||||
2, self._t("picking.sequence.selected", "Ordine personalizzato")
|
||||
)
|
||||
|
||||
ctk.CTkLabel(
|
||||
self,
|
||||
text=self._t(
|
||||
"picking.sequence.default_hint",
|
||||
"Default: prima Non scaff., poi gli scaffali secondo l'ordine WMS.",
|
||||
),
|
||||
anchor="w",
|
||||
text_color=theme_color(self._theme, "secondary_text_color", "#4b5563"),
|
||||
).grid(row=3, column=0, columnspan=3, padx=16, pady=(8, 4), sticky="ew")
|
||||
|
||||
bottom = ctk.CTkFrame(self)
|
||||
bottom.grid(row=4, column=0, columnspan=3, padx=16, pady=(6, 14), sticky="ew")
|
||||
bottom.grid_columnconfigure(0, weight=1)
|
||||
reset_button = ctk.CTkButton(
|
||||
bottom,
|
||||
text=self._t("picking.sequence.button.reset", "Ripristina default"),
|
||||
command=self._reset_default,
|
||||
fg_color=theme_color(self._theme, "reset_button_fg_color", "#6b7280"),
|
||||
)
|
||||
reset_button.grid(row=0, column=1, padx=5, pady=8)
|
||||
self._attach_tooltip(reset_button, "picking.sequence.reset")
|
||||
save_button = ctk.CTkButton(
|
||||
bottom,
|
||||
text=self._t("picking.sequence.button.save", "Salva"),
|
||||
command=self._save,
|
||||
)
|
||||
save_button.grid(row=0, column=2, padx=5, pady=8)
|
||||
self._attach_tooltip(save_button, "picking.sequence.save")
|
||||
close_button = ctk.CTkButton(
|
||||
bottom,
|
||||
text=self._t("picking.sequence.button.close", "Chiudi"),
|
||||
command=self.destroy,
|
||||
)
|
||||
close_button.grid(row=0, column=3, padx=5, pady=8)
|
||||
self._attach_tooltip(close_button, "picking.sequence.close")
|
||||
|
||||
def _attach_tooltip(self, widget: tk.Misc, key: str) -> None:
|
||||
"""Attach a localized tooltip when the catalog contains one."""
|
||||
|
||||
value = tooltip_text(key, catalog=self._tooltip_catalog) if key else ""
|
||||
if value:
|
||||
WidgetToolTip(widget, value)
|
||||
|
||||
def _build_list_panel(self, column: int, title: str):
|
||||
"""Create one titled, scrollable group list."""
|
||||
|
||||
panel = ctk.CTkFrame(self)
|
||||
panel.grid(row=2, column=column, padx=(16 if column == 0 else 0, 16 if column == 2 else 0), pady=4, sticky="nsew")
|
||||
panel.grid_columnconfigure(0, weight=1)
|
||||
panel.grid_rowconfigure(1, weight=1)
|
||||
ctk.CTkLabel(panel, text=title, font=("Segoe UI", 12, "bold")).grid(
|
||||
row=0, column=0, padx=10, pady=(10, 6), sticky="w"
|
||||
)
|
||||
content = ctk.CTkScrollableFrame(panel, fg_color=theme_color(self._theme, "list_fg_color", "#f8fafc"))
|
||||
content.grid(row=1, column=0, padx=8, pady=(0, 8), sticky="nsew")
|
||||
content.grid_columnconfigure(0, weight=1)
|
||||
return panel, content
|
||||
|
||||
def _show_and_load(self) -> None:
|
||||
"""Center the ready window and start its asynchronous load."""
|
||||
|
||||
try:
|
||||
self.update_idletasks()
|
||||
parent = self.master
|
||||
x = max(0, parent.winfo_rootx() + (parent.winfo_width() - self.winfo_width()) // 2)
|
||||
y = max(0, parent.winfo_rooty() + (parent.winfo_height() - self.winfo_height()) // 2)
|
||||
self.geometry(f"+{x}+{y}")
|
||||
except Exception:
|
||||
pass
|
||||
self.deiconify()
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self._load()
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Load available groups and the optional saved sequence."""
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(SQL_LOAD_GROUPS, {"documento": self.documento})
|
||||
|
||||
def _ok(result):
|
||||
rows = _rows_to_dicts(result)
|
||||
configured: list[tuple[int, PickupGroup]] = []
|
||||
available: list[PickupGroup] = []
|
||||
for row in rows:
|
||||
group = PickupGroup(
|
||||
key=str(row.get("GruppoPrelievoChiave") or "").strip(),
|
||||
label=str(row.get("GruppoPrelievo") or "").strip(),
|
||||
default_order=int(row.get("OrdineGruppoDefault") or 0),
|
||||
)
|
||||
configured_order = row.get("OrdineGruppoConfigurato")
|
||||
if configured_order is None:
|
||||
available.append(group)
|
||||
else:
|
||||
configured.append((int(configured_order), group))
|
||||
self.available = sorted(available, key=lambda item: (item.default_order, item.label))
|
||||
self.selected = [item for _order, item in sorted(configured, key=lambda pair: pair[0])]
|
||||
self._render_lists()
|
||||
|
||||
def _error(exc):
|
||||
messagebox.showerror(
|
||||
self._t("picking.sequence.title", "Sequenza di prelievo"),
|
||||
self._t("picking.sequence.msg.load_error", "Errore durante il caricamento:\n{error}", error=exc),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=_ok,
|
||||
on_error=_error,
|
||||
busy=self.busy,
|
||||
message=self._t("picking.sequence.busy.load", "Carico gli scaffali..."),
|
||||
)
|
||||
|
||||
def _render_lists(self) -> None:
|
||||
"""Redraw list buttons after a move or a database load."""
|
||||
|
||||
for host in (self.available_list, self.selected_list):
|
||||
for child in host.winfo_children():
|
||||
child.destroy()
|
||||
for row, group in enumerate(self.available):
|
||||
self._render_group_button(self.available_list, group, "available", row)
|
||||
for row, group in enumerate(self.selected):
|
||||
self._render_group_button(self.selected_list, group, "selected", row)
|
||||
|
||||
def _render_group_button(self, host, group: PickupGroup, side: str, row: int) -> None:
|
||||
"""Render one selectable and draggable group button."""
|
||||
|
||||
is_selected = self._selected_key == group.key and self._selected_side == side
|
||||
prefix = f"{row + 1}. " if side == "selected" else ""
|
||||
button = ctk.CTkButton(
|
||||
host,
|
||||
text=f"{prefix}{group.label}",
|
||||
anchor="w",
|
||||
height=34,
|
||||
fg_color="#1976d2" if is_selected else theme_color(self._theme, "item_fg_color", "#2ebf74"),
|
||||
command=lambda g=group, s=side: self._select(g, s),
|
||||
)
|
||||
button.grid(row=row, column=0, padx=4, pady=4, sticky="ew")
|
||||
button.bind("<Double-Button-1>", lambda _event, g=group, s=side: self._toggle_group(g, s), add="+")
|
||||
drag_widgets = [button]
|
||||
for attr in ("_canvas", "_text_label", "_image_label"):
|
||||
widget = getattr(button, attr, None)
|
||||
if widget is not None:
|
||||
drag_widgets.append(widget)
|
||||
for widget in drag_widgets:
|
||||
widget.bind("<ButtonPress-1>", lambda _event, g=group, s=side: self._start_drag(g, s), add="+")
|
||||
widget.bind("<ButtonRelease-1>", self._finish_drag, add="+")
|
||||
self._attach_tooltip(button, "picking.sequence.item")
|
||||
|
||||
def _select(self, group: PickupGroup, side: str) -> None:
|
||||
"""Select one group for the explicit move controls."""
|
||||
|
||||
self._selected_key = group.key
|
||||
self._selected_side = side
|
||||
self._render_lists()
|
||||
|
||||
def _toggle_group(self, group: PickupGroup, side: str) -> None:
|
||||
"""Move a double-clicked group to the opposite side."""
|
||||
|
||||
self._selected_key = group.key
|
||||
self._selected_side = side
|
||||
self._add_selected() if side == "available" else self._remove_selected()
|
||||
|
||||
def _selected_group(self) -> PickupGroup | None:
|
||||
"""Return the group selected in the active side."""
|
||||
|
||||
source = self.available if self._selected_side == "available" else self.selected
|
||||
return next((item for item in source if item.key == self._selected_key), None)
|
||||
|
||||
def _add_selected(self) -> None:
|
||||
"""Append the selected available group to the custom sequence."""
|
||||
|
||||
if self._selected_side != "available":
|
||||
return
|
||||
group = self._selected_group()
|
||||
if group is None:
|
||||
return
|
||||
self.available = [item for item in self.available if item.key != group.key]
|
||||
self.selected.append(group)
|
||||
self._selected_side = "selected"
|
||||
self._render_lists()
|
||||
|
||||
def _remove_selected(self) -> None:
|
||||
"""Return the selected configured group to the available list."""
|
||||
|
||||
if self._selected_side != "selected":
|
||||
return
|
||||
group = self._selected_group()
|
||||
if group is None:
|
||||
return
|
||||
self.selected = [item for item in self.selected if item.key != group.key]
|
||||
self.available.append(group)
|
||||
self.available.sort(key=lambda item: (item.default_order, item.label))
|
||||
self._selected_side = "available"
|
||||
self._render_lists()
|
||||
|
||||
def _move_selected(self, offset: int) -> None:
|
||||
"""Move one configured group up or down."""
|
||||
|
||||
if self._selected_side != "selected":
|
||||
return
|
||||
index = next((i for i, item in enumerate(self.selected) if item.key == self._selected_key), -1)
|
||||
target = index + int(offset)
|
||||
if index < 0 or target < 0 or target >= len(self.selected):
|
||||
return
|
||||
self.selected[index], self.selected[target] = self.selected[target], self.selected[index]
|
||||
self._render_lists()
|
||||
|
||||
def _start_drag(self, group: PickupGroup, side: str) -> None:
|
||||
"""Remember the item being dragged."""
|
||||
|
||||
self._drag_group = group
|
||||
self._drag_side = side
|
||||
|
||||
@staticmethod
|
||||
def _is_descendant(widget: tk.Misc | None, ancestor: tk.Misc) -> bool:
|
||||
"""Return whether a widget belongs to one list panel."""
|
||||
|
||||
current = widget
|
||||
while current is not None:
|
||||
if current == ancestor:
|
||||
return True
|
||||
current = getattr(current, "master", None)
|
||||
return False
|
||||
|
||||
def _finish_drag(self, event) -> None:
|
||||
"""Drop the dragged group into a list and place it near the pointer."""
|
||||
|
||||
group = self._drag_group
|
||||
if group is None:
|
||||
return
|
||||
self._drag_group = None
|
||||
try:
|
||||
target_widget = self.winfo_containing(event.x_root, event.y_root)
|
||||
except Exception:
|
||||
return
|
||||
if self._is_descendant(target_widget, self.selected_panel):
|
||||
self.available = [item for item in self.available if item.key != group.key]
|
||||
self.selected = [item for item in self.selected if item.key != group.key]
|
||||
target_y = event.y_root - self.selected_list.winfo_rooty()
|
||||
row_height = 42
|
||||
target_index = max(0, min(len(self.selected), target_y // row_height))
|
||||
self.selected.insert(target_index, group)
|
||||
self._selected_side = "selected"
|
||||
elif self._is_descendant(target_widget, self.available_panel):
|
||||
self.selected = [item for item in self.selected if item.key != group.key]
|
||||
if not any(item.key == group.key for item in self.available):
|
||||
self.available.append(group)
|
||||
self.available.sort(key=lambda item: (item.default_order, item.label))
|
||||
self._selected_side = "available"
|
||||
else:
|
||||
return
|
||||
self._selected_key = group.key
|
||||
self._render_lists()
|
||||
|
||||
def _save_sql(self) -> tuple[str, dict[str, Any]]:
|
||||
"""Build one atomic soft-delete/insert batch using bound parameters."""
|
||||
|
||||
params: dict[str, Any] = {
|
||||
"documento": self.documento,
|
||||
"utente": self.session.login if self.session else "SYSTEM",
|
||||
}
|
||||
values: list[str] = []
|
||||
for index, group in enumerate(self.selected):
|
||||
params[f"key_{index}"] = group.key
|
||||
params[f"label_{index}"] = group.label
|
||||
params[f"order_{index}"] = index
|
||||
values.append(
|
||||
f"(:documento, :key_{index}, :label_{index}, :order_{index}, :utente, SYSDATETIME(), :utente, SYSDATETIME())"
|
||||
)
|
||||
insert = ""
|
||||
if values:
|
||||
insert = """
|
||||
INSERT INTO dbo.py_PickingListPrelievoSequenza (
|
||||
Documento, GruppoChiave, GruppoEtichetta, Ordine,
|
||||
InsUtente, InsDataOra, ModUtente, ModDataOra
|
||||
)
|
||||
VALUES
|
||||
""" + ",\n".join(values) + ";"
|
||||
sql = f"""
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
UPDATE dbo.py_PickingListPrelievoSequenza
|
||||
SET DelUtente = :utente,
|
||||
DelDataOra = SYSDATETIME(),
|
||||
ModUtente = :utente,
|
||||
ModDataOra = SYSDATETIME()
|
||||
WHERE Documento = :documento
|
||||
AND DelDataOra IS NULL;
|
||||
{insert}
|
||||
SELECT CAST({len(values)} AS int) AS ElementiSalvati;
|
||||
"""
|
||||
return sql, params
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Persist the configured prefix atomically."""
|
||||
|
||||
sql, params = self._save_sql()
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(sql, params, commit=True)
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=lambda _result: self._save_completed("saved"),
|
||||
on_error=self._save_error,
|
||||
busy=self.busy,
|
||||
message=self._t("picking.sequence.busy.save", "Salvo la sequenza..."),
|
||||
)
|
||||
|
||||
def _reset_default(self) -> None:
|
||||
"""Delete the custom sequence so the shared default becomes active."""
|
||||
|
||||
async def _job():
|
||||
return await self.db_client.query_json(
|
||||
SQL_RESET_SEQUENCE,
|
||||
{
|
||||
"documento": self.documento,
|
||||
"utente": self.session.login if self.session else "SYSTEM",
|
||||
},
|
||||
commit=True,
|
||||
)
|
||||
|
||||
self.runner.run(
|
||||
_job(),
|
||||
on_success=lambda _result: self._save_completed("reset"),
|
||||
on_error=self._save_error,
|
||||
busy=self.busy,
|
||||
message=self._t("picking.sequence.busy.save", "Salvo la sequenza..."),
|
||||
)
|
||||
|
||||
def _save_completed(self, mode: str) -> None:
|
||||
"""Refresh the editor and notify the parent after a successful save."""
|
||||
|
||||
message_key = "picking.sequence.msg.reset" if mode == "reset" else "picking.sequence.msg.saved"
|
||||
default = (
|
||||
"Sequenza personalizzata rimossa. E' attivo l'ordine predefinito."
|
||||
if mode == "reset"
|
||||
else "Sequenza salvata."
|
||||
)
|
||||
messagebox.showinfo(self._t("picking.sequence.title", "Sequenza di prelievo"), self._t(message_key, default), parent=self)
|
||||
if self.on_saved:
|
||||
self.on_saved()
|
||||
self._selected_key = None
|
||||
self._selected_side = None
|
||||
self._load()
|
||||
|
||||
def _save_error(self, exc: BaseException) -> None:
|
||||
"""Show a localized save error."""
|
||||
|
||||
messagebox.showerror(
|
||||
self._t("picking.sequence.title", "Sequenza di prelievo"),
|
||||
self._t("picking.sequence.msg.save_error", "Errore durante il salvataggio:\n{error}", error=exc),
|
||||
parent=self,
|
||||
)
|
||||
|
||||
|
||||
def open_pickinglist_sequence_window(
|
||||
parent: tk.Misc,
|
||||
db_client,
|
||||
*,
|
||||
documento: str,
|
||||
session: UserSession | None,
|
||||
on_saved: Callable[[], None] | None = None,
|
||||
) -> PickingListSequenceWindow:
|
||||
"""Open at most one sequence editor for the picking-list window."""
|
||||
|
||||
key = "_pickinglist_sequence_window_singleton"
|
||||
existing = getattr(parent, key, None)
|
||||
if existing is not None:
|
||||
try:
|
||||
if existing.winfo_exists() and existing.documento == str(documento).strip():
|
||||
existing.on_saved = on_saved
|
||||
existing.deiconify()
|
||||
existing.lift()
|
||||
existing.focus_force()
|
||||
return existing
|
||||
if existing.winfo_exists():
|
||||
existing.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
window = PickingListSequenceWindow(
|
||||
parent,
|
||||
db_client,
|
||||
documento=documento,
|
||||
session=session,
|
||||
on_saved=on_saved,
|
||||
)
|
||||
setattr(parent, key, window)
|
||||
|
||||
def _clear_reference() -> None:
|
||||
try:
|
||||
if getattr(parent, key, None) is window:
|
||||
setattr(parent, key, None)
|
||||
finally:
|
||||
window.destroy()
|
||||
|
||||
window.protocol("WM_DELETE_WINDOW", _clear_reference)
|
||||
return window
|
||||
Reference in New Issue
Block a user