From 7ecc906d59193fba54dae8e6fa9a4452757b2990 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Fri, 3 Jul 2026 10:13:49 +0200 Subject: [PATCH] Aggiungi pausa picking list nel barcode --- barcode_client.py | 103 +++++++++++++++++++++++++++----- barcode_repository.py | 20 +++++++ barcode_service.py | 134 ++++++++++++++++++++++++++++++++++++++++-- version_info.py | 6 +- 4 files changed, 238 insertions(+), 25 deletions(-) diff --git a/barcode_client.py b/barcode_client.py index 075f220..ce89585 100644 --- a/barcode_client.py +++ b/barcode_client.py @@ -67,6 +67,7 @@ class BarcodeClientApp: self._pending: Future | None = None self._auto_advance_id: str | None = None self._pallet_auto_focus_id: str | None = None + self._paused_queue_id: int | None = None self._status_colors = { "red": "#f4cccc", "green": "#d9ead3", @@ -198,7 +199,7 @@ class BarcodeClientApp: self.btn_f1 = ttk.Button(buttons, text="[F1] H Priority", command=lambda: self._start_queue(1)) self.btn_f1.grid(row=0, column=0, padx=(0, 4), pady=(0, button_pad_y), sticky="ew") - self.btn_submit = ttk.Button(buttons, text="[Ent] Carica", command=self._submit) + self.btn_submit = ttk.Button(buttons, text="[F3] Carica", command=self._on_f3) self.btn_submit.grid(row=0, column=1, padx=(4, 0), pady=(0, button_pad_y), sticky="ew") self.btn_f2 = ttk.Button(buttons, text="[F2] L Priority", command=lambda: self._start_queue(0)) self.btn_f2.grid(row=1, column=0, padx=(0, 4), sticky="ew") @@ -324,7 +325,9 @@ class BarcodeClientApp: def _bind_keys(self) -> None: self.root.bind("", lambda _e: self._start_queue(1)) self.root.bind("", lambda _e: self._start_queue(0)) + self.root.bind("", self._on_f3_key) self.root.bind("", self._on_unload_key) + self.root.bind("", self._on_escape_key) self.pallet_entry.bind("", self._on_pallet_enter) self.destination_entry.bind("", self._on_destination_enter) @@ -352,6 +355,9 @@ class BarcodeClientApp: self.busy_bar.stop() self.busy_cover.place_forget() + def _is_priority_pause(self) -> bool: + return str(self.queue_var.get() or "") == "Pausa PL" + def _apply_state(self, state: BarcodeViewState) -> None: if self._auto_advance_id is not None: try: @@ -368,12 +374,26 @@ class BarcodeClientApp: self.queue_var.set(state.queue_label) self.destination_var.set(state.destination_barcode) self.scanned_var.set(state.scanned_pallet) - self.info1_var.set(state.status_text) + self.info1_var.set(self._status_text_with_wait_hint(state)) self.info2_var.set(state.document) self.info3_var.set(state.customer) self.info4_var.set(state.expected_pallet) self.status_band.configure(bg=state.status_color or self._status_colors["red"]) + try: + if str(state.queue_label or "") != "Pausa PL": + self._paused_queue_id = None + resumable_queue = self._resumable_queue_from_state(state) + if state.mode in ("priority_high", "priority_low") or resumable_queue is not None: + self.btn_submit.configure(text="[F3] Pausa PL") + else: + self.btn_submit.configure(text="[F3] Carica") + queue_buttons_state = "disabled" if str(state.queue_label or "") == "Pausa PL" else "normal" + self.btn_f1.configure(state=queue_buttons_state) + self.btn_f2.configure(state=queue_buttons_state) + except Exception: + pass + destination_readonly = bool(getattr(state, "destination_readonly", False)) try: self.destination_entry.configure(state="normal") @@ -382,16 +402,11 @@ class BarcodeClientApp: except Exception: pass - is_completed_move = ( - str(state.status_text or "").startswith("Ok Scarico") - or str(state.status_text or "").startswith("Ok Carico") - ) - if state.mode == "confirm" and is_completed_move: - next_queue = self._queue_id_from_label(state.queue_label) - delay_ms = int(getattr(state, "auto_advance_delay_ms", 0) or 0) - if next_queue is not None and delay_ms > 0: + if state.mode == "confirm": + next_queue = self._resumable_queue_from_state(state) + if next_queue is not None: self._auto_advance_id = self.root.after( - delay_ms, + int(getattr(state, "auto_advance_delay_ms", 0) or 0), lambda q=next_queue: self._start_queue(q), ) @@ -404,6 +419,17 @@ class BarcodeClientApp: except Exception: pass + def _status_text_with_wait_hint(self, state: BarcodeViewState) -> str: + text = str(getattr(state, "status_text", "") or "") + delay_ms = int(getattr(state, "auto_advance_delay_ms", 0) or 0) + if delay_ms <= 0: + return text + seconds = max(1, round(delay_ms / 1000)) + hint = f"Attendi {seconds} secondi..." + if hint.lower() in text.lower(): + return text + return f"{text} - {hint}" if text else hint + def _focus_destination_input(self) -> None: try: self.destination_entry.configure(state="normal") @@ -443,20 +469,50 @@ class BarcodeClientApp: self._begin_manual_unload() return "break" + def _on_escape_key(self, _event=None) -> str: + if self._is_priority_pause() and self._paused_queue_id is not None: + self._start_queue(self._paused_queue_id, allow_during_pause=True) + return "break" + + def _on_f3_key(self, _event=None) -> str: + self._on_f3() + return "break" + + def _on_f3(self) -> None: + mode = getattr(self.service.state, "mode", "") + if mode == "priority_high": + self._paused_queue_id = 1 + self._apply_state(self.service.begin_priority_pause(1)) + return + if mode == "priority_low": + self._paused_queue_id = 0 + self._apply_state(self.service.begin_priority_pause(0)) + return + resumable_queue = self._resumable_queue_from_state(self.service.state) + if resumable_queue is not None: + self._paused_queue_id = resumable_queue + self._apply_state(self.service.begin_priority_pause(resumable_queue)) + return + self._submit() + def _begin_manual_unload(self) -> None: pallet = str(self.scanned_var.get() or "").strip() destination = str(self.destination_var.get() or "").strip() - if pallet and destination in (self.NON_SCAFFALATA_BARCODE, self.SHIPPED_BARCODE): + mode = getattr(self.service.state, "mode", "") + if pallet and mode in ("priority_high", "priority_low") and destination == self.SHIPPED_BARCODE: # Legacy barcode flow: F4/Scarica confirms the prepared unload destination. self._submit() return - if pallet and not destination: - self.destination_var.set(self.NON_SCAFFALATA_BARCODE) - self._submit() + if pallet: + self._submit_unload_with_source_check() + return + if self._is_priority_pause(): return self._apply_state(self.service.begin_manual_unload()) - def _start_queue(self, id_stato: int) -> None: + def _start_queue(self, id_stato: int, *, allow_during_pause: bool = False) -> None: + if self._is_priority_pause() and not allow_during_pause: + return self._run_async( lambda: self.service.start_priority_queue(id_stato), busy_message="In preparazione...", @@ -471,6 +527,15 @@ class BarcodeClientApp: busy_message="In esecuzione...", ) + def _submit_unload_with_source_check(self) -> None: + self._run_async( + lambda: self.service.submit_unload_with_source_check( + scanned_pallet=self.scanned_var.get(), + source_barcode=self.destination_var.get(), + ), + busy_message="In esecuzione...", + ) + def _run_async(self, coro_factory: Callable[[], object], busy_message: str) -> None: if self._pending is not None and not self._pending.done(): return @@ -492,6 +557,12 @@ class BarcodeClientApp: return 0 return None + def _resumable_queue_from_state(self, state: BarcodeViewState) -> int | None: + delay_ms = int(getattr(state, "auto_advance_delay_ms", 0) or 0) + if delay_ms <= 0: + return None + return self._queue_id_from_label(str(getattr(state, "queue_label", "") or "")) + def _poll_future(self) -> None: if self._pending is None: self._set_busy(False) diff --git a/barcode_repository.py b/barcode_repository.py index 6ce3c70..42206f1 100644 --- a/barcode_repository.py +++ b/barcode_repository.py @@ -72,6 +72,20 @@ LEFT JOIN dbo.Celle AS c WHERE g.BarcodePallet = :pallet; """ +SQL_OPEN_LOCATIONS_BY_PALLET = """ +SELECT + g.BarcodePallet, + g.IDCella, + RTRIM(c.Corsia) AS Corsia, + RTRIM(CAST(c.Colonna AS varchar(32))) AS Colonna, + RTRIM(CAST(c.Fila AS varchar(32))) AS Fila +FROM dbo.XMag_GiacenzaPallet AS g +LEFT JOIN dbo.Celle AS c + ON c.ID = g.IDCella +WHERE g.BarcodePallet = :pallet +ORDER BY g.IDCella; +""" + SQL_RESOLVE_PHYSICAL_CELL = """ DECLARE @raw int = TRY_CONVERT(int, :destination); DECLARE @cell_id int = @@ -184,6 +198,12 @@ class BarcodeRepository: rows = _rows_to_dicts(res) return rows[0] if rows else None + async def fetch_open_locations_by_pallet(self, pallet: str) -> list[dict[str, Any]]: + """Return all positive stock locations currently open for a pallet.""" + + res = await self.db_client.query_json(SQL_OPEN_LOCATIONS_BY_PALLET, {"pallet": str(pallet or "").strip()}) + return _rows_to_dicts(res) + async def resolve_physical_cell(self, destination: str) -> DestinationCell | None: """Accept either an internal cell ID or the scanned legacy cell barcode.""" diff --git a/barcode_service.py b/barcode_service.py index ef69bde..c163a4e 100644 --- a/barcode_service.py +++ b/barcode_service.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import Literal from barcode_repository import BarcodeRepository, LegacyMoveResult -from runtime_support import log_exception +from runtime_support import log_exception, log_runtime_event from version_info import module_version __version__ = module_version(__name__) @@ -58,7 +58,7 @@ class BarcodeService: def __init__(self, repository: BarcodeRepository, operator_id: int): self.repository = repository self.operator_id = int(operator_id) - self._current_priority_state = 0 + self._current_priority_state = -1 self._state = BarcodeViewState() @property @@ -70,14 +70,14 @@ class BarcodeService: def reset(self) -> BarcodeViewState: """Return the client to its neutral state.""" - self._current_priority_state = 0 + self._current_priority_state = -1 self._state = BarcodeViewState() return self._state def begin_manual_load(self) -> BarcodeViewState: """Prepare a real versamento into a physical warehouse cell.""" - self._current_priority_state = 0 + self._current_priority_state = -1 self._state = BarcodeViewState( mode="manual_load", queue_label="Versamento", @@ -89,7 +89,7 @@ class BarcodeService: def begin_manual_unload(self) -> BarcodeViewState: """Prepare a direct unload toward the conventional non-shelved cell.""" - self._current_priority_state = 0 + self._current_priority_state = -1 self._state = BarcodeViewState( mode="manual_unload", queue_label="Prelievo diretto", @@ -100,6 +100,20 @@ class BarcodeService: ) return self._state + def begin_priority_pause(self, id_stato: int) -> BarcodeViewState: + """Pause the active picking queue locally for exactly one free movement.""" + + self._current_priority_state = int(id_stato) + self._state = BarcodeViewState( + mode="manual_unload", + queue_label="Pausa PL", + status_text="Premi ESC per uscire dalla pausa", + status_color=self.GRAY, + destination_barcode=self.NON_SCAFFALATA_BARCODE, + destination_readonly=False, + ) + return self._state + async def start_priority_queue(self, id_stato: int) -> BarcodeActionResult: """Load the next item of the selected legacy priority queue.""" @@ -107,6 +121,7 @@ class BarcodeService: self._current_priority_state = int(id_stato) queue_label = "Alta priorita' (F1)" if int(id_stato) == 1 else "Bassa priorita' (F2)" if not row: + self._current_priority_state = -1 self._state = BarcodeViewState( mode="manual_unload", queue_label=queue_label, @@ -169,6 +184,27 @@ class BarcodeService: self._state.status_color = self.RED return BarcodeActionResult(False, self._state, self._state.status_text) + open_locations = await self.repository.fetch_open_locations_by_pallet(pallet) + distinct_cells = sorted({ + int(row.get("IDCella") or 0) + for row in open_locations + if row.get("IDCella") is not None + }) + if len(distinct_cells) > 1: + log_runtime_event( + "Barcode WMS", + ( + "MOVE BLOCKED MULTI_LOCATION " + f"pallet={pallet} " + f"cells={','.join(str(cell) for cell in distinct_cells)}" + ), + ) + self._state.scanned_pallet = pallet + self._state.destination_barcode = destination + self._state.status_text = "UDC presente in piu' celle. Movimento bloccato." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + target_barcode = destination target_numero_cella = int(destination) target_id_cella = 9999 if destination == self.SHIPPED_BARCODE else (1000 if destination == self.NON_SCAFFALATA_BARCODE else None) @@ -210,7 +246,7 @@ class BarcodeService: destination_barcode=destination, destination_display=target_display, last_priority_state=self._current_priority_state, - auto_advance_delay_ms=5000 if (is_direct_unload or is_direct_load) else 1200 if is_picking_unload else 0, + auto_advance_delay_ms=5000 if (is_direct_unload or is_direct_load) else 3000 if is_picking_unload else 0, ) except Exception as exc: log_exception("Barcode WMS", exc, context=f"post move state pallet={pallet} destination={destination}") @@ -224,6 +260,92 @@ class BarcodeService: ) return BarcodeActionResult(True, self._state, self._state.status_text) + async def submit_unload_with_source_check(self, *, scanned_pallet: str, source_barcode: str) -> BarcodeActionResult: + """Unload to the non-shelved cell only if the scanned source cell matches DB.""" + + pallet = str(scanned_pallet or "").strip() + source = str(source_barcode or "").strip() + if not pallet: + return BarcodeActionResult(False, self._state, "Inserisci o leggi il pallet.") + if not source: + return BarcodeActionResult(False, self._state, "Leggi il codice della cella da scaricare.") + if not source.isdigit(): + return BarcodeActionResult(False, self._state, "La cella da scaricare deve essere numerica.") + + current_location = await self.repository.fetch_current_location_by_pallet(pallet) + if not current_location: + self._state.scanned_pallet = pallet + self._state.status_text = "UDC non presente a magazzino." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + open_locations = await self.repository.fetch_open_locations_by_pallet(pallet) + distinct_cells = sorted({ + int(row.get("IDCella") or 0) + for row in open_locations + if row.get("IDCella") is not None + }) + if len(distinct_cells) > 1: + log_runtime_event( + "Barcode WMS", + ( + "MOVE BLOCKED MULTI_LOCATION " + f"pallet={pallet} " + f"cells={','.join(str(cell) for cell in distinct_cells)}" + ), + ) + self._state.scanned_pallet = pallet + self._state.destination_barcode = source + self._state.status_text = "UDC presente in piu' celle. Movimento bloccato." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + if source == self.NON_SCAFFALATA_BARCODE: + source_id_cella = 1000 + else: + resolved_source = await self.repository.resolve_physical_cell(source) + if not resolved_source: + self._state.scanned_pallet = pallet + self._state.destination_barcode = source + self._state.status_text = f"Cella non valida: {source}." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + source_id_cella = int(resolved_source.id_cella) + + current_id_cella = int((current_location or {}).get("IDCella") or 0) + if current_id_cella != source_id_cella: + if source == self.NON_SCAFFALATA_BARCODE and current_id_cella != 1000: + log_runtime_event( + "Barcode WMS", + ( + "MOVE BLOCKED SOURCE_REQUIRED " + f"pallet={pallet} " + f"db_cell={current_id_cella} " + f"default_cell={source_id_cella}" + ), + ) + self._state.scanned_pallet = pallet + self._state.destination_barcode = source + self._state.status_text = "Errore: leggi la cella da scaricare." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + log_runtime_event( + "Barcode WMS", + ( + "MOVE BLOCKED SOURCE_MISMATCH " + f"pallet={pallet} " + f"db_cell={current_id_cella} " + f"scanned_cell={source_id_cella}" + ), + ) + self._state.scanned_pallet = pallet + self._state.destination_barcode = source + self._state.status_text = "Errore: UDC in cella diversa, rileggi il codice della cella da scaricare." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + return await self.submit(scanned_pallet=pallet, destination_barcode=self.NON_SCAFFALATA_BARCODE) + async def _build_post_move_state( self, *, diff --git a/version_info.py b/version_info.py index 453f8b9..cdc4c98 100644 --- a/version_info.py +++ b/version_info.py @@ -13,9 +13,9 @@ MODULE_VERSIONS: dict[str, str] = { "async_msssql_query": "1.0.0", "audit_log": "1.0.0", "main": "1.0.1", - "barcode_client": "1.0.12", - "barcode_repository": "1.0.3", - "barcode_service": "1.0.7", + "barcode_client": "1.0.17", + "barcode_repository": "1.0.4", + "barcode_service": "1.0.13", "busy_overlay": "1.0.0", "db_config": "1.0.0", "gestione_aree": "1.0.1",