From a5e704c214a0013cc8af239d05ec1be990fd36b8 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Fri, 22 May 2026 14:25:09 +0200 Subject: [PATCH] Milestone ultima alpha --- analisi_bug_prenotazione_pickinglist.md | 588 ++++++++++++++++++++++++ barcode_client.py | 481 +++++++++++++++++++ barcode_repository.py | 153 ++++++ barcode_service.py | 247 ++++++++++ db_config.py | 427 +++++++++++++++++ diagramma_scarico_udc.md | 115 +++++ flussi operativi.txt | 78 ++++ gestione_layout.py | 24 +- gestione_pickinglist.py | 77 ++-- gestione_scarico.py | 63 ++- locale.json | 197 ++++++++ locale_text.py | 41 ++ login_window.py | 143 ++++-- main.py | 118 ++--- patch_sp_xExePackingListPallet.sql | 119 +++++ prenota_sprenota_sql.py | 121 ++--- requirements.txt | 8 + reset_corsie.py | 12 +- search_pallets.py | 139 +++++- spec_barcode_wms.rtf | 383 +++++++++++++++ spec_barcode_wms_aggiornata.rtf | 170 +++++++ tooltip.json | 24 + ui_theme.json | 106 +++++ view_celle_multi_udc.py | 66 ++- window_placement.py | 269 ++++++++++- 25 files changed, 3896 insertions(+), 273 deletions(-) create mode 100644 analisi_bug_prenotazione_pickinglist.md create mode 100644 barcode_client.py create mode 100644 barcode_repository.py create mode 100644 barcode_service.py create mode 100644 db_config.py create mode 100644 diagramma_scarico_udc.md create mode 100644 flussi operativi.txt create mode 100644 locale.json create mode 100644 locale_text.py create mode 100644 patch_sp_xExePackingListPallet.sql create mode 100644 requirements.txt create mode 100644 spec_barcode_wms.rtf create mode 100644 spec_barcode_wms_aggiornata.rtf diff --git a/analisi_bug_prenotazione_pickinglist.md b/analisi_bug_prenotazione_pickinglist.md new file mode 100644 index 0000000..defa8bc --- /dev/null +++ b/analisi_bug_prenotazione_pickinglist.md @@ -0,0 +1,588 @@ +# Analisi bug prenotazione Picking List + +## Obiettivo + +Documentare in modo chiaro: + +- il comportamento attuale della prenotazione picking list +- il bug osservato +- la causa tecnica del bug +- la correzione proposta lato stored procedure + +Il documento serve come base di riferimento prima di modificare la logica SQL legacy. + +--- + +## Contesto operativo + +L'operatore, dal backoffice: + +1. seleziona una picking list +2. preme `Prenota` +3. si aspetta che **una sola** picking list risulti prenotata + +Il comportamento atteso del sistema è: + +- una sola picking list prenotata alla volta +- quella picking list diventa la coda `F1` sul barcode +- le altre picking list restano non prenotate e alimentano la coda `F2` + +--- + +## Comportamento attuale + +Il comportamento effettivamente osservato è questo: + +1. l'operatore seleziona una sola picking list +2. preme `Prenota` +3. risultano prenotate **due** picking list, non una sola + +Questo comportamento è stato osservato: + +- nell'applicazione Python +- anche nell'applicazione C# + +Quindi il bug **non nasce nella UI Python**. + +--- + +## Verifica di aderenza Python / C# + +### Lato Python + +La finestra [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py) chiama: + +- [sp_xExePackingListPallet_async](C:/devel/python/ware_house/prenota_sprenota_sql.py) + +che esegue direttamente la stored procedure SQL legacy: + +- `dbo.sp_xExePackingListPallet` + +### Lato C# + +La finestra legacy [FRMagViewLayout.cs](C:/devel/seawall_decompiled/decompiled/SWMagViewLayout/FRMagViewLayout.cs) chiama anch'essa: + +- `dbo.sp_xExePackingListPallet` + +### Conclusione + +Python e C# usano la **stessa** logica DB per la prenotazione. + +Se il bug compare in entrambi, il problema è nella stored procedure o nel modello dati su cui essa si basa. + +--- + +## Stato attuale della griglia picking list + +La query summary della picking list in [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py) aggrega così: + +- `GROUP BY Documento, CodNazione, NAZIONE, Stato` +- `MAX(IDStato) AS IDStato` + +Questo significa: + +- se anche **una sola riga** del documento ha `IDStato = 1` +- l'intera picking list appare come prenotata + +Questa scelta è coerente con l'idea UI di “documento prenotato”, ma diventa problematica se la stored non lavora in modo esclusivo per documento. + +--- + +## Stored procedure attuale + +Stored coinvolta: + +- [sp_xExePackingListPallet](C:/devel/python/ware_house/script.sql) + +### Logica attuale + +La stored: + +1. legge da `XMag_ViewPackingList` +2. estrae le `Cella` associate al `Documento` +3. per ogni cella: + - se `IDStato = 0`, la mette a `1` + - altrimenti la rimette a `0` + +Quindi la stored non prenota davvero una picking list come entità esclusiva. + +Prenota invece: + +- **le celle** toccate dal documento selezionato + +### Conseguenza tecnica + +Se due documenti condividono almeno una stessa cella: + +- la prenotazione della prima list mette `IDStato = 1` sulla cella condivisa +- anche la seconda list, leggendo quella stessa cella, eredita `IDStato = 1` +- la griglia alta, usando `MAX(IDStato)`, la mostra come prenotata + +--- + +## Evidenze raccolte + +### Query 1 - celle del sottoinsieme di documenti osservati + +Scopo: + +- verificare quali celle sono coinvolte dai documenti sospetti +- controllare rapidamente se esistono sovrapposizioni evidenti + +Query: + +```sql +SELECT + Documento, + Cella, + COUNT(*) AS Righe +FROM dbo.XMag_ViewPackingList +WHERE Documento IN (, ) +GROUP BY Documento, Cella +ORDER BY Cella, Documento; +``` + +Uso nel caso analizzato: + +- ha permesso di vedere, per esempio, che documenti diversi insistono sulla stessa cella `8057` + +### Query di controllo celle condivise + +Risultato: + +```text +1000 2 +8057 2 +``` + +Interpretazione: + +- la cella `1000` è condivisa da 2 documenti +- la cella `8057` è condivisa da 2 documenti + +### Query di dettaglio documenti / celle condivise + +Risultato: + +```text +135 1000 16 16 +137 1000 2 1 +133 8057 1 1 +135 8057 1 1 +``` + +Interpretazione: + +- documento `135` e documento `137` condividono la cella `1000` +- documento `133` e documento `135` condividono la cella `8057` + +Questa è una prova concreta del fatto che la prenotazione a livello cella può propagarsi a più documenti. + +### Query 2 - ricerca globale delle celle condivise + +Scopo: + +- trovare quali celle sono condivise da più documenti +- identificare i punti strutturalmente più critici del dataset + +Query: + +```sql +SELECT + Cella, + COUNT(DISTINCT Documento) AS NumDocumenti +FROM dbo.XMag_ViewPackingList +GROUP BY Cella +HAVING COUNT(DISTINCT Documento) > 1 +ORDER BY NumDocumenti DESC, Cella; +``` + +Risultato osservato: + +```text +1000 2 +8057 2 +``` + +Interpretazione: + +- la cella `1000` è condivisa da 2 documenti +- la cella `8057` è condivisa da 2 documenti + +### Query 3 - dettaglio completo delle collisioni sulle celle condivise + +Scopo: + +- capire esattamente quali documenti condividono le celle critiche +- vedere quante righe e quanti pallet distinti insistono su quelle celle + +Query: + +```sql +SELECT + Documento, + Cella, + COUNT(*) AS Righe, + COUNT(DISTINCT Pallet) AS Pallet +FROM dbo.XMag_ViewPackingList +WHERE Cella IN (1000, 8057) +GROUP BY Documento, Cella +ORDER BY Cella, Documento; +``` + +Risultato osservato: + +```text +135 1000 16 16 +137 1000 2 1 +133 8057 1 1 +135 8057 1 1 +``` + +Interpretazione: + +- documento `135` e documento `137` condividono la cella `1000` +- documento `133` e documento `135` condividono la cella `8057` + +Queste tre query, lette insieme, sono quelle che hanno permesso di evidenziare in modo oggettivo il bug. + +--- + +## Ruolo delle celle convenzionali + +Dalle verifiche fatte con il magazziniere: + +- `1000` = `5E1.1` +- `9999` = `7G.1.1` + +La cella `1000` è una locazione convenzionale delle UDC non scaffalate. + +Questo rende il problema ancora più frequente, perché: + +- più documenti possono convivere sulla stessa locazione convenzionale `1000` +- quindi la prenotazione per cella tende naturalmente a contaminare più picking list + +--- + +## Diagnosi finale del bug + +Il bug è dovuto alla combinazione di due scelte: + +1. la stored `sp_xExePackingListPallet` lavora a livello **cella** +2. la UI mostra la prenotazione a livello **documento** + +Quando esistono celle condivise tra documenti: + +- la prenotazione di un documento produce `IDStato = 1` anche su righe lette da altri documenti +- quindi più picking list risultano prenotate + +### Forma sintetica del bug + +> Il sistema non sta prenotando una singola picking list in modo esclusivo. +> Sta prenotando le celle associate a quel documento, e la UI deduce da lì lo stato della picking list. + +--- + +# Correzione proposta + +## Obiettivo della correzione + +Allineare il comportamento del sistema alla regola di business desiderata: + +> una sola picking list prenotata per volta + +Questo implica che il comando `Prenota` deve produrre uno stato esclusivo a livello documento. + +--- + +## Principio della nuova logica + +La stored non deve più comportarsi come un semplice toggle delle celle del documento selezionato. + +Deve invece: + +1. rimuovere la prenotazione da tutte le altre picking list +2. applicare la prenotazione solo al documento selezionato + +In altre parole: + +- **prima** si resetta lo stato prenotato degli altri documenti +- **poi** si prenota il documento scelto + +--- + +## Comportamento desiderato dopo la correzione + +### Caso `Prenota` + +Quando l'operatore prenota il documento `D`: + +1. se il documento `D` è già prenotato, non succede nulla +2. se il documento `D` non è prenotato: + - tutte le celle prenotate appartenenti ad altri documenti vengono riportate a `IDStato = 0` + - tutte le celle del documento `D` vengono portate a `IDStato = 1` + - il log della picking list resta aggiornato come oggi + +Effetto atteso: + +- solo il documento `D` risulta prenotato nella griglia +- solo il documento `D` alimenta la coda `F1` +- tutte le altre picking list alimentano `F2` +- premere `Prenota` più volte sulla stessa picking list già prenotata non deve produrre toggle né effetti collaterali + +### Caso `S-prenota` + +Quando l'operatore s-prenota il documento `D`: + +1. se il documento `D` non è prenotato, non succede nulla +2. se il documento `D` è prenotato: + - le celle del documento `D` vengono riportate a `IDStato = 0` + - nessun altro documento viene automaticamente prenotato + +Effetto atteso: + +- nessuna picking list resta prenotata, salvo una nuova prenotazione esplicita +- premere `S-prenota` più volte sulla stessa picking list già non prenotata non deve produrre toggle né effetti collaterali + +--- + +## Strategia tecnica consigliata + +### Approccio minimo e prudente + +La correzione più sicura, restando vicini al legacy, è mantenere una stored unica ma con una semantica esplicita guidata da un parametro azione. + +Proposta: + +- stored unica con parametro `@Azione` + - `P` = Prenota + - `S` = S-prenota + +Logica: + +1. determinare se il documento selezionato è già prenotato o no +2. se `@Azione = 'P'` + - se il documento è già prenotato: non fare nulla + - altrimenti: + - azzerare `IDStato = 1` sulle celle coinvolte da altri documenti prenotati + - impostare `IDStato = 1` sulle celle del documento selezionato +3. se `@Azione = 'S'` + - se il documento non è prenotato: non fare nulla + - altrimenti: + - riportare a `0` solo le celle del documento selezionato + +Questa logica mantiene: + +- la semantica esistente a livello cella +- ma aggiunge l'esclusività a livello documento +- e mantiene pulita la semantica distinta dei pulsanti `Prenota` e `S-prenota` + +### Vantaggi + +- modifica contenuta +- comportamento coerente con l'operatività reale +- nessuna necessità immediata di cambiare UI Python o C# +- comportamento idempotente dei pulsanti + +--- + +## Rischi da considerare + +### 1. Celle condivise tra documenti + +La presenza di celle condivise resta un'anomalia logica del dominio. + +Anche con la correzione proposta: + +- se una cella appartiene contemporaneamente a più documenti nella vista +- la semantica “quale documento possiede davvero la prenotazione della cella” resta concettualmente debole + +Tuttavia la correzione proposta risolve il bug visibile di doppia picking list prenotata. + +### 2. Effetti sul flusso barcode + +Il barcode usa `IDStato = 1` per la coda `F1`. + +Quindi la nuova esclusività deve essere verificata attentamente su: + +- proposta UDC in `F1` +- proposta UDC in `F2` +- avanzamento dopo prelievo +- ri-prenotazione automatica residua via `sp_ControllaPrenotazionePackingListPalletNew` + +### 3. Interazione con `LogPackingList` + +La stored attuale aggiorna il log del documento. + +Questa parte va mantenuta, perché il barcode e la logica di ri-prenotazione residua si appoggiano su quel log. + +--- + +## Criteri di accettazione della correzione + +La correzione sarà considerata valida se, dopo `Prenota`: + +1. una sola picking list risulta prenotata in griglia +2. `F1` propone solo UDC del documento prenotato +3. `F2` propone UDC delle altre picking list +4. il problema di doppia prenotazione non si ripresenta più nemmeno in presenza di celle condivise come `1000` o `8057` + +E dopo `S-prenota`: + +1. il documento selezionato torna non prenotato +2. `F1` non propone più quella picking list + +--- + +## Decisione consigliata + +La correzione lato stored procedure è consigliata e necessaria. + +Motivo: + +- il bug è reale +- il bug è condiviso da Python e C# +- il bug nasce dalla logica DB attuale +- la prenotazione esclusiva è coerente con il modello operativo del magazzino + +--- + +## Stored procedure proposta + +Di seguito una proposta documentale di stored unica con parametro `P` / `S`. + +```sql +CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPallet] + @IDOperatore int, + @Documento varchar(8), + @Azione char(1), -- 'P' = Prenota, 'S' = S-prenota + @RC int OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET @RC = 0; + + DECLARE @Nominativo varchar(50); + DECLARE @DocumentoPrenotato bit = 0; + DECLARE @Description varchar(255) = ''; + DECLARE @Message varchar(255) = ''; + DECLARE @IDResult int = 0; + + SELECT @Nominativo = [Login] + FROM dbo.Operatori + WHERE ID = @IDOperatore; + + IF @Azione NOT IN ('P', 'S') + BEGIN + SET @RC = -10; + RETURN; + END; + + IF OBJECT_ID('tempdb..#TargetCelle') IS NOT NULL DROP TABLE #TargetCelle; + CREATE TABLE #TargetCelle ( + IDCella int PRIMARY KEY + ); + + INSERT INTO #TargetCelle (IDCella) + SELECT DISTINCT Cella + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento + AND Cella IS NOT NULL; + + IF NOT EXISTS (SELECT 1 FROM #TargetCelle) + BEGIN + SET @RC = -20; + RETURN; + END; + + IF EXISTS ( + SELECT 1 + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento + AND ISNULL(IDStato, 0) = 1 + ) + BEGIN + SET @DocumentoPrenotato = 1; + END; + + IF @Azione = 'P' + BEGIN + -- Gia' prenotata: no-op + IF @DocumentoPrenotato = 1 + RETURN; + + -- Azzera prenotazioni di altri documenti + UPDATE c + SET c.IDStato = 0, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + WHERE c.ID IN ( + SELECT DISTINCT Cella + FROM dbo.XMag_ViewPackingList + WHERE ISNULL(IDStato, 0) = 1 + AND Documento <> @Documento + AND Cella IS NOT NULL + ); + + -- Prenota solo il documento target + UPDATE c + SET c.IDStato = 1, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + INNER JOIN #TargetCelle t ON t.IDCella = c.ID; + + SELECT TOP 1 @Description = NAZIONE + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento; + + EXEC dbo.sp_LogPackingList + @ID = 0, + @Code = @Documento, + @Description = @Description, + @Message = @Message OUTPUT, + @IDResult = @IDResult OUTPUT; + + RETURN; + END; + + IF @Azione = 'S' + BEGIN + -- Gia' non prenotata: no-op + IF @DocumentoPrenotato = 0 + RETURN; + + -- S-prenota solo il documento target + UPDATE c + SET c.IDStato = 0, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + INNER JOIN #TargetCelle t ON t.IDCella = c.ID; + + RETURN; + END; +END; +``` + +### Note sulla stored proposta + +- Non esiste più il toggle implicito. +- `Prenota` è idempotente: + - se la list è già prenotata, non cambia nulla. +- `S-prenota` è idempotente: + - se la list è già non prenotata, non cambia nulla. +- L'esclusività viene garantita solo nel ramo `P`. +- La logica continua a usare `IDStato` sulle celle, così il barcode legacy può continuare a usare `F1` / `F2` con la semantica attuale. + +--- + +## Prossimo passo + +Dopo approvazione di questo documento: + +1. modificare `sp_xExePackingListPallet` o introdurre una nuova versione compatibile +2. testare il risultato con almeno 3 picking list attive +3. verificare il comportamento sul backoffice +4. verificare il comportamento sul barcode (`F1` / `F2`) diff --git a/barcode_client.py b/barcode_client.py new file mode 100644 index 0000000..0d510ee --- /dev/null +++ b/barcode_client.py @@ -0,0 +1,481 @@ +"""Lightweight standalone barcode client for the warehouse WMS.""" + +from __future__ import annotations + +import asyncio +import sys +import tkinter as tk +from concurrent.futures import Future +from tkinter import messagebox, ttk +from typing import Callable + +from async_loop_singleton import get_global_loop, stop_global_loop +from async_msssql_query import AsyncMSSQLClient +from barcode_repository import BarcodeRepository +from barcode_service import BarcodeActionResult, BarcodeService, BarcodeViewState +from db_config import build_dsn_from_config, ensure_db_config +from login_window import prompt_login_compact + + +if sys.platform.startswith("win"): + try: + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + except Exception: + pass + + +class BarcodeClientApp: + """Single-window Tk barcode client modeled after the C# legacy form.""" + + BARCODE_MAX_WIDTH = 320 + BARCODE_MAX_HEIGHT = 400 + DESKTOP_THRESHOLD_WIDTH = 1024 + DESKTOP_THRESHOLD_HEIGHT = 768 + DESKTOP_WINDOW_WIDTH = 465 + DESKTOP_WINDOW_HEIGHT = 531 + + def __init__(self, root: tk.Tk, db_client: AsyncMSSQLClient, session, loop: asyncio.AbstractEventLoop): + self.root = root + self.db_client = db_client + self.session = session + self.loop = loop + self.repository = BarcodeRepository(db_client) + self.service = BarcodeService(self.repository, session.operator_id) + self._pending: Future | None = None + self._auto_advance_id: str | None = None + self._status_colors = { + "red": "#f4cccc", + "green": "#d9ead3", + "yellow": "#e2f0cb", + } + self._is_barcode_desktop = False + + self.queue_var = tk.StringVar(value="") + self.info1_var = tk.StringVar(value="") + self.info2_var = tk.StringVar(value="") + self.info3_var = tk.StringVar(value="") + self.info4_var = tk.StringVar(value="") + self.destination_var = tk.StringVar(value="") + self.scanned_var = tk.StringVar(value="") + + self._build_ui() + self._apply_state(self.service.state) + self._bind_keys() + self.root.protocol("WM_DELETE_WINDOW", self._shutdown) + + def _build_ui(self) -> None: + self.root.title("WMS") + self.root.configure(bg="#f1f1f1") + self._apply_responsive_geometry() + + if self._is_barcode_desktop: + field_font = ("Segoe UI", 9) + entry_font = ("Segoe UI", 11) + band_font = ("Segoe UI", 9, "bold") + info_font = ("Segoe UI", 9) + queue_font = ("Segoe UI", 8) + pad_x = 5 + pad_y = 4 + band_wrap = 190 + info_wrap = 194 + button_pad_y = 1 + else: + field_font = ("Segoe UI", 12) + entry_font = ("Segoe UI", 15) + band_font = ("Segoe UI", 12, "bold") + info_font = ("Segoe UI", 12) + queue_font = ("Segoe UI", 10) + pad_x = 16 + pad_y = 12 + band_wrap = 400 + info_wrap = 408 + button_pad_y = 4 + + wrap = tk.Frame(self.root, bg="#f1f1f1", padx=pad_x, pady=pad_y) + wrap.pack(fill="both", expand=True) + wrap.rowconfigure(3, weight=1) + wrap.columnconfigure(0, weight=1) + + header = tk.Frame(wrap, bg="#f1f1f1") + header.grid(row=0, column=0, sticky="ew", pady=(0, 4 if self._is_barcode_desktop else 10)) + tk.Label( + header, + textvariable=self.queue_var, + bg="#f1f1f1", + fg="#333333", + font=queue_font, + anchor="e", + ).pack(side="right") + + form = tk.Frame(wrap, bg="#f1f1f1") + form.grid(row=1, column=0, sticky="ew") + self._add_compact_field(form, 0, "Pallet", self.scanned_var, scanned=True, label_font=field_font, entry_font=entry_font) + self._add_compact_field(form, 1, "Cella", self.destination_var, scanned=False, label_font=field_font, entry_font=entry_font) + + self.status_band = tk.Label( + wrap, + textvariable=self.info1_var, + bg=self._status_colors["red"], + fg="#1f2937", + font=band_font, + anchor="w", + justify="left", + padx=6, + pady=3 if self._is_barcode_desktop else 6, + wraplength=band_wrap, + ) + self.status_band.grid(row=2, column=0, sticky="ew", pady=(8 if self._is_barcode_desktop else 14, 4)) + + info_box = tk.Frame(wrap, bg="#f1f1f1") + info_box.grid(row=3, column=0, sticky="nsew") + info_box.columnconfigure(0, weight=1) + for row_index in range(3): + info_box.rowconfigure(row_index, weight=1) + self.info2_label = tk.Label( + info_box, + textvariable=self.info2_var, + bg="#f1f1f1", + fg="#222222", + font=info_font, + anchor="w", + justify="left", + wraplength=info_wrap, + ) + self.info2_label.grid(row=0, column=0, sticky="ew", pady=(2, 0)) + self.info3_label = tk.Label( + info_box, + textvariable=self.info3_var, + bg="#f1f1f1", + fg="#222222", + font=info_font, + anchor="w", + justify="left", + wraplength=info_wrap, + ) + self.info3_label.grid(row=1, column=0, sticky="ew", pady=(2, 0)) + self.info4_label = tk.Label( + info_box, + textvariable=self.info4_var, + bg="#f1f1f1", + fg="#222222", + font=info_font, + anchor="w", + justify="left", + wraplength=info_wrap, + ) + self.info4_label.grid(row=2, column=0, sticky="ew", pady=(2, 4 if self._is_barcode_desktop else 10)) + + buttons = tk.Frame(wrap, bg="#f1f1f1") + buttons.grid(row=4, column=0, sticky="ew", pady=(4, 0)) + buttons.columnconfigure(0, weight=1) + buttons.columnconfigure(1, weight=1) + + 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] Salva", command=self._submit) + 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") + self.btn_unload = ttk.Button(buttons, text="[F4] Elimina", command=self._begin_manual_unload) + self.btn_unload.grid(row=1, column=1, padx=(4, 0), sticky="ew") + + self.busy_cover = tk.Frame(self.root, bg="#d9d9d9") + panel = ttk.Frame(self.busy_cover, padding=16) + panel.place(relx=0.5, rely=0.5, anchor="center") + self.busy_label = ttk.Label(panel, text="Attendere...", font=("Segoe UI", 11, "bold")) + self.busy_label.pack(pady=(0, 8)) + self.busy_bar = ttk.Progressbar(panel, mode="indeterminate", length=260) + self.busy_bar.pack() + def _add_compact_field( + self, + parent: tk.Frame, + row: int, + label: str, + variable: tk.StringVar, + *, + scanned: bool, + label_font, + entry_font, + ) -> None: + tk.Label( + parent, + text=label, + bg="#f1f1f1", + fg="#1f1f1f", + font=label_font, + anchor="w", + ).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4 if self._is_barcode_desktop else 6) + entry = ttk.Entry(parent, textvariable=variable, font=entry_font, width=8) + entry.grid(row=row, column=1, sticky="ew", pady=4 if self._is_barcode_desktop else 6, ipady=1 if self._is_barcode_desktop else 4) + parent.columnconfigure(1, weight=1) + if scanned: + self.pallet_entry = entry + self.scanned_var.trace_add("write", lambda *_: self._limit_var(self.scanned_var, 8)) + else: + self.destination_entry = entry + self.destination_var.trace_add("write", lambda *_: self._limit_var(self.destination_var, 8)) + + def _limit_var(self, variable: tk.StringVar, max_len: int) -> None: + value = str(variable.get() or "") + if len(value) > max_len: + variable.set(value[:max_len]) + + def _apply_responsive_geometry(self) -> None: + """Adapt the window size to barcode-sized or desktop-sized screens.""" + + self.root.update_idletasks() + screen_width = max(1, int(self.root.winfo_screenwidth() or 0)) + screen_height = max(1, int(self.root.winfo_screenheight() or 0)) + + is_barcode_desktop = ( + screen_width <= self.BARCODE_MAX_WIDTH or screen_height <= self.BARCODE_MAX_HEIGHT + ) + self._is_barcode_desktop = is_barcode_desktop + + if is_barcode_desktop: + width = screen_width + height = screen_height + x = 0 + y = 0 + self.root.minsize(max(220, width), max(300, height)) + else: + width = min(self.DESKTOP_WINDOW_WIDTH, screen_width) + height = min(self.DESKTOP_WINDOW_HEIGHT, screen_height) + x = 0 + y = 0 + self.root.minsize(420, 500) + + if ( + screen_width >= self.DESKTOP_THRESHOLD_WIDTH + and screen_height >= self.DESKTOP_THRESHOLD_HEIGHT + and not is_barcode_desktop + ): + width = min(self.DESKTOP_WINDOW_WIDTH, screen_width) + height = min(self.DESKTOP_WINDOW_HEIGHT, screen_height) + x = 0 + y = 0 + + self.root.geometry(f"{width}x{height}+{x}+{y}") + + 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("", lambda _e: self._begin_manual_unload()) + self.pallet_entry.bind("", self._on_pallet_enter) + self.destination_entry.bind("", self._on_destination_enter) + + def _set_busy(self, busy: bool, message: str = "") -> None: + state = "disabled" if busy else "normal" + for button in (self.btn_f1, self.btn_f2, self.btn_unload, self.btn_submit): + try: + button.configure(state=state) + except Exception: + pass + try: + self.pallet_entry.configure(state=state) + except Exception: + pass + try: + if str(self.destination_entry.cget("state")) != "readonly" or busy: + self.destination_entry.configure(state=state) + except Exception: + pass + if busy: + self.busy_label.configure(text=message or "Attendere...") + self.busy_cover.place(relx=0, rely=0, relwidth=1, relheight=1) + self.busy_bar.start(10) + else: + self.busy_bar.stop() + self.busy_cover.place_forget() + + def _apply_state(self, state: BarcodeViewState) -> None: + if self._auto_advance_id is not None: + try: + self.root.after_cancel(self._auto_advance_id) + except Exception: + pass + self._auto_advance_id = None + 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.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"]) + + destination_readonly = state.mode in ("priority_high", "priority_low", "manual_unload") + try: + self.destination_entry.configure(state="normal") + if destination_readonly: + self.destination_entry.configure(state="readonly") + except Exception: + pass + + if state.mode == "confirm" and state.status_text == "Ok Scarico": + next_queue = self._queue_id_from_label(state.queue_label) + if next_queue is not None: + self._auto_advance_id = self.root.after(1200, lambda q=next_queue: self._start_queue(q)) + + self.root.after(20, self._focus_primary_input) + + def _focus_primary_input(self) -> None: + try: + self.pallet_entry.focus_force() + self.pallet_entry.selection_range(0, "end") + except Exception: + pass + + def _focus_destination_input(self) -> None: + try: + self.destination_entry.configure(state="normal") + except Exception: + pass + try: + self.destination_entry.focus_force() + self.destination_entry.selection_range(0, "end") + except Exception: + pass + + def _on_pallet_enter(self, _event=None) -> str: + pallet = str(self.scanned_var.get() or "").strip() + destination = str(self.destination_var.get() or "").strip() + if not pallet: + return "break" + if destination == "9000000": + self._submit() + return "break" + self.destination_var.set("") + self._focus_destination_input() + return "break" + + def _on_destination_enter(self, _event=None) -> str: + pallet = str(self.scanned_var.get() or "").strip() + destination = str(self.destination_var.get() or "").strip() + if pallet and destination: + self._submit() + else: + self._focus_primary_input() + return "break" + + def _begin_manual_unload(self) -> None: + self._apply_state(self.service.begin_manual_unload()) + + def _start_queue(self, id_stato: int) -> None: + self._run_async( + lambda: self.service.start_priority_queue(id_stato), + busy_message="Carico la coda selezionata...", + ) + + def _submit(self) -> None: + self._run_async( + lambda: self.service.submit( + scanned_pallet=self.scanned_var.get(), + destination_barcode=self.destination_var.get(), + ), + busy_message="Eseguo il movimento...", + ) + + def _run_async(self, coro_factory: Callable[[], object], busy_message: str) -> None: + if self._pending is not None and not self._pending.done(): + return + if self._auto_advance_id is not None: + try: + self.root.after_cancel(self._auto_advance_id) + except Exception: + pass + self._auto_advance_id = None + self._set_busy(True, busy_message) + self._pending = asyncio.run_coroutine_threadsafe(coro_factory(), self.loop) + self.root.after(40, self._poll_future) + + def _queue_id_from_label(self, queue_label: str) -> int | None: + text = str(queue_label or "") + if text.startswith("Alta priorita'"): + return 1 + if text.startswith("Bassa priorita'"): + return 0 + return None + + def _poll_future(self) -> None: + if self._pending is None: + self._set_busy(False) + return + if not self._pending.done(): + self.root.after(40, self._poll_future) + return + + future = self._pending + self._pending = None + self._set_busy(False) + try: + result = future.result() + except Exception as exc: + current = self.service.state + current.status_text = f"Errore operativo: {exc}" + current.status_color = "#f4cccc" + self._apply_state(current) + messagebox.showerror("Barcode WMS", f"Operazione fallita:\n{exc}", parent=self.root) + return + + if isinstance(result, BarcodeActionResult): + self._apply_state(result.state) + if result.message and not result.ok: + self.root.bell() + elif isinstance(result, BarcodeViewState): + self._apply_state(result) + + def _shutdown(self) -> None: + try: + if self._pending is not None and not self._pending.done(): + return + except Exception: + pass + try: + fut = asyncio.run_coroutine_threadsafe(self.db_client.dispose(), self.loop) + fut.result(timeout=2) + except Exception: + pass + try: + self.root.destroy() + finally: + try: + stop_global_loop() + except Exception: + pass + + +def main() -> int: + loop = get_global_loop() + bootstrap = tk.Tk() + bootstrap.withdraw() + + config = ensure_db_config(loop, parent=bootstrap) + if not config: + bootstrap.destroy() + stop_global_loop() + return 1 + + db_client = AsyncMSSQLClient(build_dsn_from_config(config)) + session = prompt_login_compact(bootstrap, db_client) + if session is None: + try: + fut = asyncio.run_coroutine_threadsafe(db_client.dispose(), loop) + fut.result(timeout=2) + except Exception: + pass + bootstrap.destroy() + stop_global_loop() + return 1 + + app = BarcodeClientApp(bootstrap, db_client, session, loop) + bootstrap.deiconify() + bootstrap.lift() + bootstrap.focus_force() + app._focus_primary_input() + bootstrap.mainloop() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/barcode_repository.py b/barcode_repository.py new file mode 100644 index 0000000..1f72980 --- /dev/null +++ b/barcode_repository.py @@ -0,0 +1,153 @@ +"""Low-level DB access for the barcode WMS client. + +The goal of this module is to mirror the legacy C# barcode form as closely as +possible while keeping SQL isolated from the UI. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +SQL_NEXT_PICKING = """ +SELECT TOP (1) + Documento, + CodNazione, + NAZIONE, + Stato, + Pallet, + Cella, + Ubicazione, + Ordinamento, + IDStato +FROM dbo.XMag_ViewPackingList +WHERE Ordinamento > 0 + AND IDStato = :id_stato +ORDER BY Ordinamento; +""" + +SQL_PICKING_BY_PALLET = """ +SELECT TOP (1) + Documento, + CodNazione, + NAZIONE, + Stato, + Pallet, + Cella, + Ubicazione, + Ordinamento, + IDStato +FROM dbo.XMag_ViewPackingList +WHERE Pallet = :pallet +ORDER BY Ordinamento; +""" + +SQL_TRACE_BY_PALLET = """ +SELECT TOP (1) + Pallet, + Lotto, + Prodotto, + Descrizione +FROM dbo.vXTracciaProdotti +WHERE Pallet = :pallet +ORDER BY Lotto; +""" + +SQL_LEGACY_MOVE = """ +SET NOCOUNT ON; +DECLARE @RC int = 0; + +EXEC dbo.sp_xMagGestioneMagazziniPallet + @IDOperatore = :id_operatore, + @BarcodeCella = :barcode_cella, + @BarcodePallet = :barcode_pallet, + @NumeroCella = :numero_cella, + @RC = @RC OUTPUT; + +SELECT + @RC AS RC, + :barcode_cella AS BarcodeCella, + :barcode_pallet AS BarcodePallet, + :numero_cella AS NumeroCella; +""" + + +def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]: + """Convert ``query_json`` payloads to a list of row dictionaries.""" + + if not isinstance(res, dict): + return [] + rows = res.get("rows") or [] + cols = res.get("columns") or [] + if rows and isinstance(rows[0], dict): + return [row for row in rows if isinstance(row, dict)] + out: list[dict[str, Any]] = [] + for row in rows: + if isinstance(row, (list, tuple)) and cols: + out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))}) + return out + + +@dataclass +class LegacyMoveResult: + """Result of one movement executed through the legacy stored procedure.""" + + rc: int + barcode_cella: str + barcode_pallet: str + numero_cella: int + + +class BarcodeRepository: + """Thin async repository used by the lightweight barcode client.""" + + def __init__(self, db_client): + self.db_client = db_client + + async def fetch_next_picking(self, id_stato: int) -> dict[str, Any] | None: + """Return the next pallet proposed by the legacy F1/F2 queue logic.""" + + res = await self.db_client.query_json(SQL_NEXT_PICKING, {"id_stato": int(id_stato)}) + rows = _rows_to_dicts(res) + return rows[0] if rows else None + + async def fetch_picking_by_pallet(self, pallet: str) -> dict[str, Any] | None: + """Return one picking row for the given pallet, if still present in the queue.""" + + res = await self.db_client.query_json(SQL_PICKING_BY_PALLET, {"pallet": str(pallet or "").strip()}) + rows = _rows_to_dicts(res) + return rows[0] if rows else None + + async def fetch_trace_by_pallet(self, pallet: str) -> dict[str, Any] | None: + """Return trace information used by the C# fallback confirmation path.""" + + res = await self.db_client.query_json(SQL_TRACE_BY_PALLET, {"pallet": str(pallet or "").strip()}) + rows = _rows_to_dicts(res) + return rows[0] if rows else None + + async def execute_legacy_move( + self, + *, + operator_id: int, + barcode_cella: str, + barcode_pallet: str, + numero_cella: int, + ) -> LegacyMoveResult: + """Execute the same stored procedure used by the C# barcode form.""" + + params = { + "id_operatore": int(operator_id), + "barcode_cella": str(barcode_cella or "").strip(), + "barcode_pallet": str(barcode_pallet or "").strip(), + "numero_cella": int(numero_cella), + } + res = await self.db_client.query_json(SQL_LEGACY_MOVE, params, commit=True) + rows = _rows_to_dicts(res) + row = rows[0] if rows else {} + return LegacyMoveResult( + rc=int(row.get("RC") or 0), + barcode_cella=str(row.get("BarcodeCella") or params["barcode_cella"]), + barcode_pallet=str(row.get("BarcodePallet") or params["barcode_pallet"]), + numero_cella=int(row.get("NumeroCella") or params["numero_cella"]), + ) diff --git a/barcode_service.py b/barcode_service.py new file mode 100644 index 0000000..efffd26 --- /dev/null +++ b/barcode_service.py @@ -0,0 +1,247 @@ +"""Service layer for the lightweight barcode WMS client.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +from barcode_repository import BarcodeRepository, LegacyMoveResult + + +ModeName = Literal["idle", "priority_high", "priority_low", "manual_load", "manual_unload", "confirm"] + + +@dataclass +class BarcodeViewState: + """State projected from service logic into the barcode UI.""" + + mode: ModeName = "idle" + queue_label: str = "" + status_text: str = "Pronto." + status_color: str = "#d9d9d9" + source_location: str = "" + document: str = "" + customer: str = "" + expected_pallet: str = "" + destination_barcode: str = "" + scanned_pallet: str = "" + + +@dataclass +class BarcodeActionResult: + """Standard result returned by user actions.""" + + ok: bool + state: BarcodeViewState + message: str = "" + + +class BarcodeService: + """Faithful, but cleaner, port of the legacy barcode form logic.""" + + GRAY = "#d9d9d9" + RED = "#f4cccc" + LIGHT_GREEN = "#d9ead3" + GREEN_YELLOW = "#e2f0cb" + CONVENTIONAL_LOCATION_BY_CELL = { + 1000: "5E1.1", + 9999: "7G.1.1", + } + + def __init__(self, repository: BarcodeRepository, operator_id: int): + self.repository = repository + self.operator_id = int(operator_id) + self._current_priority_state = 0 + self._state = BarcodeViewState() + + @property + def state(self) -> BarcodeViewState: + """Return a copy-safe reference to the current UI state.""" + + return self._state + + def reset(self) -> BarcodeViewState: + """Return the client to its neutral state.""" + + self._current_priority_state = 0 + 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._state = BarcodeViewState( + mode="manual_load", + queue_label="Versamento", + status_text="OP Carico", + status_color=self.GRAY, + ) + return self._state + + def begin_manual_unload(self) -> BarcodeViewState: + """Prepare a direct unload toward the virtual outbound cell 9000000.""" + + self._current_priority_state = 0 + self._state = BarcodeViewState( + mode="manual_unload", + queue_label="Prelievo diretto", + status_text="OP Scarico", + status_color=self.GRAY, + destination_barcode="9000000", + ) + return self._state + + async def start_priority_queue(self, id_stato: int) -> BarcodeActionResult: + """Load the next item of the selected legacy priority queue.""" + + row = await self.repository.fetch_next_picking(id_stato) + 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._state = BarcodeViewState( + mode="priority_high" if int(id_stato) == 1 else "priority_low", + queue_label=queue_label, + status_text="Pronto.", + status_color=self.RED, + destination_barcode="9000000", + ) + return BarcodeActionResult(True, self._state) + + customer = f"{row.get('CodNazione') or ''} - {row.get('NAZIONE') or ''}".strip(" -") + source_location = self._display_location( + cella=row.get("Cella"), + ubicazione=row.get("Ubicazione"), + ) + self._state = BarcodeViewState( + mode="priority_high" if int(id_stato) == 1 else "priority_low", + queue_label=queue_label, + status_text=f"Ok Cella - {source_location}", + status_color=self.LIGHT_GREEN, + source_location=source_location, + document=str(row.get("Documento") or ""), + customer=customer, + expected_pallet=str(row.get("Pallet") or ""), + destination_barcode="9000000", + ) + return BarcodeActionResult(True, self._state) + + async def submit(self, *, scanned_pallet: str, destination_barcode: str) -> BarcodeActionResult: + """Execute the movement according to the current mode.""" + + pallet = str(scanned_pallet or "").strip() + destination = str(destination_barcode or "").strip() + if not pallet: + return BarcodeActionResult(False, self._state, "Inserisci o leggi il pallet.") + if not destination: + return BarcodeActionResult(False, self._state, "Inserisci o leggi la destinazione.") + if not destination.isdigit(): + return BarcodeActionResult(False, self._state, "La destinazione deve essere numerica.") + + if self._state.mode in ("priority_high", "priority_low") and destination == "9000000": + expected = str(self._state.expected_pallet or "").strip() + if expected and expected != pallet: + self._state.scanned_pallet = pallet + self._state.status_text = "Errata lettura: il pallet letto non coincide con quello atteso." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + move = await self.repository.execute_legacy_move( + operator_id=self.operator_id, + barcode_cella=destination, + barcode_pallet=pallet, + numero_cella=int(destination), + ) + if move.rc != 0: + self._state.scanned_pallet = pallet + self._state.status_text = f"Operazione non riuscita (RC={move.rc})." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + self._state = await self._build_post_move_state( + barcode_pallet=pallet, + destination_barcode=destination, + last_priority_state=self._current_priority_state, + ) + return BarcodeActionResult(True, self._state, self._state.status_text) + + async def _build_post_move_state( + self, + *, + barcode_pallet: str, + destination_barcode: str, + last_priority_state: int, + ) -> BarcodeViewState: + """Mirror the legacy confirmation flow after one stored-procedure move.""" + + picking_row = await self.repository.fetch_picking_by_pallet(barcode_pallet) + if picking_row: + customer = f"{picking_row.get('CodNazione') or ''} - {picking_row.get('NAZIONE') or ''}".strip(" -") + source_location = self._display_location( + cella=picking_row.get("Cella"), + ubicazione=picking_row.get("Ubicazione"), + ) + return BarcodeViewState( + mode="confirm", + queue_label="Alta priorita' (F1)" if last_priority_state == 1 else ("Bassa priorita' (F2)" if last_priority_state == 0 else ""), + status_text=f"Ok Cella - {source_location}", + status_color=self.LIGHT_GREEN, + source_location=source_location, + document=str(picking_row.get("Documento") or ""), + customer=customer, + expected_pallet=str(picking_row.get("Pallet") or ""), + destination_barcode=destination_barcode, + ) + + trace_row = await self.repository.fetch_trace_by_pallet(barcode_pallet) + if trace_row: + lotto = str(trace_row.get("Lotto") or "") + prodotto = str(trace_row.get("Prodotto") or "") + descrizione = str(trace_row.get("Descrizione") or "") + queue_label = "Alta priorita' (F1)" if last_priority_state == 1 else ("Bassa priorita' (F2)" if last_priority_state == 0 else "Conferma movimento") + return BarcodeViewState( + mode="confirm", + queue_label=queue_label, + status_text=( + "Ok Scarico" if destination_barcode == "9000000" else f"Ok Carico - {destination_barcode}" + ), + status_color=self.GREEN_YELLOW, + source_location=str(destination_barcode or ""), + document=( + self.CONVENTIONAL_LOCATION_BY_CELL[9999] + if destination_barcode == "9000000" and last_priority_state in (0, 1) + else lotto + ), + customer=( + lotto + if destination_barcode == "9000000" and last_priority_state in (0, 1) + else prodotto + ), + expected_pallet=( + " - ".join(part for part in (prodotto, descrizione) if part) + if destination_barcode == "9000000" and last_priority_state in (0, 1) + else descrizione + ), + destination_barcode=destination_barcode, + scanned_pallet=barcode_pallet, + ) + + return BarcodeViewState( + mode="confirm", + queue_label="Conferma movimento", + status_text="Movimento eseguito.", + status_color=self.GREEN_YELLOW, + destination_barcode=destination_barcode, + scanned_pallet=barcode_pallet, + ) + + def _display_location(self, *, cella: object, ubicazione: object) -> str: + """Return the operator-facing location, honoring legacy conventional cells.""" + + try: + cella_int = int(cella) + except Exception: + cella_int = None + if cella_int in self.CONVENTIONAL_LOCATION_BY_CELL: + return self.CONVENTIONAL_LOCATION_BY_CELL[cella_int] + return str(ubicazione or cella or "") diff --git a/db_config.py b/db_config.py new file mode 100644 index 0000000..fd60156 --- /dev/null +++ b/db_config.py @@ -0,0 +1,427 @@ +"""Database connection bootstrap helpers for first-run configuration.""" + +from __future__ import annotations + +import asyncio +import json +import tkinter as tk +from pathlib import Path +from tkinter import messagebox, ttk +from typing import Any + +from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn +from locale_text import load_locale_catalog, text as loc_text +from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text +from ui_theme import theme_section, theme_value + +CONFIG_PATH = Path(__file__).with_name("db_connection.json") +DEFAULT_DB_CONFIG: dict[str, Any] = { + "server": r"mde3\gesterp", + "database": "Mediseawall", + "user": "sa", + "password": "1Password1", + "driver": "ODBC Driver 17 for SQL Server", + "trust_server_certificate": True, + "encrypt": "", +} + + +def load_db_config(path: Path = CONFIG_PATH) -> dict[str, Any] | None: + """Return the DB config from disk, or ``None`` when missing/invalid.""" + + try: + if not path.exists(): + return None + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + return None + return {**DEFAULT_DB_CONFIG, **data} + except Exception: + return None + + +def save_db_config(config: dict[str, Any], path: Path = CONFIG_PATH) -> None: + """Persist the DB config as UTF-8 JSON.""" + + payload = {**DEFAULT_DB_CONFIG, **config} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + +def build_dsn_from_config(config: dict[str, Any]) -> str: + """Build the SQLAlchemy DSN from the saved configuration.""" + + return make_mssql_dsn( + server=str(config.get("server") or "").strip(), + database=str(config.get("database") or "").strip(), + user=str(config.get("user") or "").strip() or None, + password=str(config.get("password") or "").strip() or None, + driver=str(config.get("driver") or DEFAULT_DB_CONFIG["driver"]).strip(), + trust_server_certificate=bool(config.get("trust_server_certificate", True)), + encrypt=(str(config.get("encrypt") or "").strip() or None), + ) + + +def _is_complete(config: dict[str, Any] | None) -> bool: + """Return True when the config contains the required connection fields.""" + + if not isinstance(config, dict): + return False + required = ("server", "database", "user", "password") + return all(str(config.get(key) or "").strip() for key in required) + + +def test_db_config_sync(config: dict[str, Any], loop: asyncio.AbstractEventLoop, timeout: float = 6.0) -> None: + """Raise an exception when the DB configuration cannot open a connection.""" + + client = AsyncMSSQLClient(build_dsn_from_config(config), log=False) + + async def _job() -> None: + try: + await client.query_json("SELECT 1 AS Ok", {}, as_dict_rows=True) + finally: + try: + await client.dispose() + except Exception: + pass + + fut = asyncio.run_coroutine_threadsafe(_job(), loop) + fut.result(timeout=timeout) + + +class DatabaseConfigWindow(tk.Toplevel): + """Modal first-run form that collects the SQL Server connection settings.""" + + def __init__(self, parent: tk.Misc, loop: asyncio.AbstractEventLoop, initial: dict[str, Any] | None = None): + super().__init__(parent) + self._loop = loop + self._theme = theme_section("db_config_window", {}) + self._locale_catalog = load_locale_catalog() + self._tooltip_catalog = load_tooltip_catalog() + self.result_config: dict[str, Any] | None = None + merged = {**DEFAULT_DB_CONFIG, **(initial or {})} + + self.title(loc_text("dbconfig.title", catalog=self._locale_catalog, default="Configurazione Database")) + self.geometry(str(theme_value(self._theme, "window_geometry", "520x360"))) + self.resizable(False, False) + try: + if parent is not None and parent.winfo_viewable(): + self.transient(parent) + except Exception: + pass + self.protocol("WM_DELETE_WINDOW", self._on_cancel) + + self.server_var = tk.StringVar(value=str(merged.get("server") or "")) + self.database_var = tk.StringVar(value=str(merged.get("database") or "")) + self.user_var = tk.StringVar(value=str(merged.get("user") or "")) + self.password_var = tk.StringVar(value=str(merged.get("password") or "")) + self.driver_var = tk.StringVar(value=str(merged.get("driver") or DEFAULT_DB_CONFIG["driver"])) + self.encrypt_var = tk.StringVar(value=str(merged.get("encrypt") or "")) + self.tsc_var = tk.BooleanVar(value=bool(merged.get("trust_server_certificate", True))) + self._status_var = tk.StringVar(value="") + + self._busy_cover: tk.Frame | None = None + self._busy_label: ttk.Label | None = None + self._busy_bar: ttk.Progressbar | None = None + self._build_ui() + self.update_idletasks() + req_w = self.winfo_reqwidth() + req_h = self.winfo_reqheight() + try: + current_w, current_h = [int(v) for v in str(theme_value(self._theme, "window_geometry", "520x360")).split("x", 1)] + except Exception: + current_w, current_h = 520, 360 + final_w = max(current_w, req_w + 16) + final_h = max(current_h, req_h + 20) + self.geometry(f"{final_w}x{final_h}") + self.minsize(final_w, final_h) + self.grab_set() + self.deiconify() + self.lift() + self.attributes("-topmost", True) + self.after(50, self._show_ready) + + def _build_ui(self) -> None: + body = ttk.Frame(self, padding=14) + body.pack(fill="both", expand=True) + body.columnconfigure(1, weight=1) + + heading = ttk.Label( + body, + text=loc_text( + "dbconfig.heading", + catalog=self._locale_catalog, + default="Configura la connessione al database del magazzino", + ), + font=("Segoe UI", 11, "bold"), + ) + heading.grid(row=0, column=0, columnspan=2, sticky="w", pady=(2, 12)) + + fields = [ + ("dbconfig.label.server", "Server", self.server_var, "dbconfig.field.server"), + ("dbconfig.label.database", "Database", self.database_var, "dbconfig.field.database"), + ("dbconfig.label.user", "Utente", self.user_var, "dbconfig.field.user"), + ("dbconfig.label.password", "Password", self.password_var, "dbconfig.field.password"), + ("dbconfig.label.driver", "Driver ODBC", self.driver_var, "dbconfig.field.driver"), + ("dbconfig.label.encrypt", "Encrypt", self.encrypt_var, "dbconfig.field.encrypt"), + ] + + self._entries: list[ttk.Entry] = [] + for row_idx, (key, default, var, tip_key) in enumerate(fields, start=1): + label = ttk.Label(body, text=loc_text(key, catalog=self._locale_catalog, default=default)) + label.grid( + row=row_idx, column=0, sticky="w", padx=(0, 10), pady=6 + ) + entry = ttk.Entry(body, textvariable=var, width=34, show="*" if var is self.password_var else "") + entry.grid(row=row_idx, column=1, sticky="ew", pady=6) + self._entries.append(entry) + self._attach_tooltip(label, tip_key) + self._attach_tooltip(entry, tip_key) + + tsc = ttk.Checkbutton( + body, + text=loc_text( + "dbconfig.label.trust_server_certificate", + catalog=self._locale_catalog, + default="Trust server certificate", + ), + variable=self.tsc_var, + ) + tsc.grid(row=7, column=0, columnspan=2, sticky="w", pady=(8, 4)) + self._attach_tooltip(tsc, "dbconfig.field.trust_server_certificate") + + info = ttk.Label( + body, + text=loc_text( + "dbconfig.info", + catalog=self._locale_catalog, + default="Il file verra' salvato localmente e non verra' piu' richiesto ai prossimi avvii.", + ), + wraplength=420, + justify="left", + ) + info.grid(row=8, column=0, columnspan=2, sticky="w", pady=(6, 8)) + + ttk.Label(body, textvariable=self._status_var, foreground="#555555").grid( + row=9, column=0, columnspan=2, sticky="w", pady=(2, 2) + ) + + actions = ttk.Frame(body) + actions.grid(row=10, column=0, columnspan=2, sticky="ew", pady=(10, 0)) + actions.columnconfigure(0, weight=1) + self._cancel_btn = ttk.Button( + actions, + text=loc_text("dbconfig.button.cancel", catalog=self._locale_catalog, default="Annulla"), + command=self._on_cancel, + ) + self._cancel_btn.grid(row=0, column=1, padx=(0, 8)) + self._attach_tooltip(self._cancel_btn, "dbconfig.button.cancel") + self._test_btn = ttk.Button( + actions, + text=loc_text("dbconfig.button.test", catalog=self._locale_catalog, default="Test connessione"), + command=self._on_test, + ) + self._test_btn.grid(row=0, column=2, padx=(0, 8)) + self._attach_tooltip(self._test_btn, "dbconfig.button.test") + self._save_btn = ttk.Button( + actions, + text=loc_text("dbconfig.button.save", catalog=self._locale_catalog, default="Salva"), + command=self._on_save, + ) + self._save_btn.grid(row=0, column=3) + self._attach_tooltip(self._save_btn, "dbconfig.button.save") + + self._attach_tooltip(heading, "dbconfig.heading") + self._attach_tooltip(info, "dbconfig.info") + + def _attach_tooltip(self, widget: tk.Misc, key: str) -> None: + """Attach a localized tooltip when a text exists for the given key.""" + + tip = tooltip_text(key, catalog=self._tooltip_catalog) + if tip: + WidgetToolTip(widget, tip) + + def _show_ready(self) -> None: + """Ensure the modal is visible even with a hidden bootstrap root.""" + + try: + self.attributes("-topmost", True) + self.deiconify() + self.lift() + self.focus_force() + if self._entries: + self._entries[0].focus_force() + finally: + try: + self.after(250, lambda: self.attributes("-topmost", False)) + except Exception: + pass + + def _show_busy_overlay(self, message: str) -> None: + """Show a lightweight inline overlay without CustomTkinter callbacks.""" + + if self._busy_cover and self._busy_cover.winfo_exists(): + if self._busy_label is not None: + self._busy_label.configure(text=message) + try: + self._busy_cover.lift() + except Exception: + pass + return + + cover = tk.Frame(self, bg="#d9d9d9") + cover.place(relx=0, rely=0, relwidth=1, relheight=1) + panel = ttk.Frame(cover, padding=14) + panel.place(relx=0.5, rely=0.5, anchor="center") + label = ttk.Label(panel, text=message, font=("Segoe UI", 10, "bold")) + label.pack(padx=16, pady=(4, 8)) + bar = ttk.Progressbar(panel, mode="indeterminate", length=220) + bar.pack(padx=16, pady=(0, 6)) + try: + bar.start(10) + except Exception: + pass + self._busy_cover = cover + self._busy_label = label + self._busy_bar = bar + + def _hide_busy_overlay(self) -> None: + """Hide the lightweight inline overlay.""" + + if self._busy_bar is not None: + try: + self._busy_bar.stop() + except Exception: + pass + self._busy_bar = None + self._busy_label = None + if self._busy_cover is not None and self._busy_cover.winfo_exists(): + try: + self._busy_cover.destroy() + except Exception: + pass + self._busy_cover = None + + def _set_busy(self, busy: bool, message: str = "") -> None: + state = "disabled" if busy else "normal" + try: + for entry in self._entries: + entry.configure(state=state) + self._cancel_btn.configure(state=state) + self._test_btn.configure(state=state) + self._save_btn.configure(state=state) + self.configure(cursor="watch" if busy else "") + self._status_var.set(message) + if busy: + self._show_busy_overlay(message or loc_text("dbconfig.busy", catalog=self._locale_catalog, default="Verifico...")) + else: + self._hide_busy_overlay() + self.update_idletasks() + except Exception: + pass + + def _collect(self) -> dict[str, Any]: + return { + "server": str(self.server_var.get() or "").strip(), + "database": str(self.database_var.get() or "").strip(), + "user": str(self.user_var.get() or "").strip(), + "password": str(self.password_var.get() or "").strip(), + "driver": str(self.driver_var.get() or "").strip() or str(DEFAULT_DB_CONFIG["driver"]), + "encrypt": str(self.encrypt_var.get() or "").strip(), + "trust_server_certificate": bool(self.tsc_var.get()), + } + + def _validate(self, config: dict[str, Any]) -> bool: + required = ("server", "database", "user", "password") + missing = [name for name in required if not str(config.get(name) or "").strip()] + if not missing: + return True + messagebox.showwarning( + loc_text("dbconfig.msg.title", catalog=self._locale_catalog, default="Configurazione Database"), + loc_text( + "dbconfig.msg.missing", + catalog=self._locale_catalog, + default="Compila almeno server, database, utente e password.", + ), + parent=self, + ) + return False + + def _test(self, config: dict[str, Any]) -> None: + self._set_busy(True, loc_text("dbconfig.busy", catalog=self._locale_catalog, default="Verifico connessione...")) + try: + test_db_config_sync(config, self._loop) + finally: + self._set_busy(False, "") + + def _on_test(self) -> None: + config = self._collect() + if not self._validate(config): + return + try: + self._test(config) + except Exception as ex: + messagebox.showerror( + loc_text("dbconfig.msg.title", catalog=self._locale_catalog, default="Configurazione Database"), + loc_text("dbconfig.msg.test_error", catalog=self._locale_catalog, default="Connessione fallita:\n{error}").format(error=ex), + parent=self, + ) + return + messagebox.showinfo( + loc_text("dbconfig.msg.title", catalog=self._locale_catalog, default="Configurazione Database"), + loc_text("dbconfig.msg.test_ok", catalog=self._locale_catalog, default="Connessione riuscita."), + parent=self, + ) + + def _on_save(self) -> None: + config = self._collect() + if not self._validate(config): + return + try: + self._test(config) + save_db_config(config) + except Exception as ex: + messagebox.showerror( + loc_text("dbconfig.msg.title", catalog=self._locale_catalog, default="Configurazione Database"), + loc_text("dbconfig.msg.save_error", catalog=self._locale_catalog, default="Salvataggio fallito:\n{error}").format(error=ex), + parent=self, + ) + return + self.result_config = config + self.destroy() + + def _on_cancel(self) -> None: + self.result_config = None + try: + self.destroy() + except Exception: + pass + + +def ensure_db_config(loop: asyncio.AbstractEventLoop, parent: tk.Misc | None = None) -> dict[str, Any] | None: + """Return a valid DB config, prompting the user the first time when needed.""" + + existing = load_db_config() + if _is_complete(existing): + return existing + + owns_root = False + if parent is None: + parent = tk.Tk() + parent.geometry("1x1+0+0") + parent.overrideredirect(True) + parent.attributes("-alpha", 0.0) + parent.deiconify() + parent.update_idletasks() + owns_root = True + + dlg = DatabaseConfigWindow(parent, loop=loop, initial=existing or DEFAULT_DB_CONFIG) + try: + parent.wait_window(dlg) + finally: + if owns_root: + try: + parent.destroy() + except Exception: + pass + return dlg.result_config diff --git a/diagramma_scarico_udc.md b/diagramma_scarico_udc.md new file mode 100644 index 0000000..5d3de76 --- /dev/null +++ b/diagramma_scarico_udc.md @@ -0,0 +1,115 @@ +# Diagramma Operativo - Scarico UDC / Prelievo + +## Obiettivo +Prelevare una UDC dal magazzino e scaricarla verso la cella virtuale `9000000`. + +## Nota importante +Dal codice C# emergono **due sottocasi diversi**: + +- `scarico picking list` + - parte da `F1` o `F2` + - valida il pallet atteso della coda +- `scarico diretto` + - parte dal pulsante `F4 Elimina` + - non richiede una picking list prenotata + +Questo diagramma descrive il **primo scarico UDC diretto**, cioe' il prelievo senza navigazione picking list. + +## Stato iniziale del barcode + +- form aperta +- nessuna operazione pendente +- focus sul campo `Pallet` +- prima label di stato neutra o grigia +- campo `Cella` non significativo finche' non si entra nel comando + +## Come si entra nello stato iniziale dello scarico + +Dal comportamento C# la strada piu' fedele e': + +1. l'operatore preme `F4` +2. la form entra in modalita' scarico diretto +3. la prima label deve indicare: + - `OP Scarico` +4. il campo `Cella` viene preimpostato a: + - `9000000` +5. il focus va sul campo `Pallet` + +## Stato operativo durante lo scarico + +- `Pallet` = da leggere +- `Cella` = `9000000` +- focus sul campo `Pallet` +- label 1 grigia con: + - `OP Scarico` + +## Sequenza operativa + +```mermaid +flowchart TD + A["Stato neutro"] --> B["Operatore preme F4"] + B --> C["Form entra in OP Scarico"] + C --> D["Cella preimpostata a 9000000"] + D --> E["Focus sul campo Pallet"] + E --> F["Operatore legge barcode pallet"] + F --> G{"Invio automatico del lettore o Enter manuale"} + G --> H["Esecuzione stored sp_xMagGestioneMagazziniPallet"] + H --> I{"Esito OK?"} + I -- Si --> L["Label 1 verde/giallo: Ok Scarico"] + L --> M["Label 2 = lotto"] + M --> N["Label 3 = codice prodotto"] + N --> O["Label 4 = descrizione articolo"] + O --> P["Focus torna su Pallet per operazione successiva"] + I -- No --> Q["Label 1 rossa con errore"] + Q --> R["Focus torna su Pallet"] +``` + +## Stato finale se l'operazione va bene + +- prima label: + - verde chiaro o giallo-verde + - testo tipo `Ok Scarico` +- seconda label: + - lotto del pallet movimentato +- terza label: + - codice prodotto +- quarta label: + - descrizione articolo +- focus: + - torna sul campo `Pallet` +- campo `Cella`: + - resta `9000000` + +## Stato finale se l'operazione fallisce + +- prima label rossa +- testo di errore operativo +- focus sul campo `Pallet` +- nessun avanzamento di coda + +## Coerenza con il C# + +I punti dedotti direttamente dal codice C# sono: + +- `F4 Elimina` forza uno scarico verso `9000000` +- lo scarico usa: + - `sp_xMagGestioneMagazziniPallet` +- dopo il movimento il C# richiama: + - `GetDatiPallet(...)` + - e se il pallet non e' piu' nella vista picking passa a: + - `GetDatiPalletLotto(...)` +- da quest'ultima lettura arrivano: + - `Ok Scarico` + - lotto + - codice prodotto + - descrizione articolo + +## Punto ancora da verificare sul campo + +Da confermare in prova reale: + +- se nel client legacy il colore di successo finale dello scarico diretto sia: + - verde chiaro + - oppure giallo-verde +- se il focus torni sempre al campo `Pallet` anche dopo errore +- se il lettore genera davvero `Enter` automatico in ogni scenario di scansione diff --git a/flussi operativi.txt b/flussi operativi.txt new file mode 100644 index 0000000..83d4f36 --- /dev/null +++ b/flussi operativi.txt @@ -0,0 +1,78 @@ +Flusso delle procedure del barcode così come le compie il magazziniere con il barcode. +Le procudere analizzate sono 3: carico(versamento), scarico(prelievo), prelievo pickinglist + +1 reset iniziale + +Il magazziniere entra nello stato iniziale premendo f1 +Poichè non c'è nessuna pickinglist prenotata (stato 1) , (questo è il pre-requisito) questo è lo stato iniziale + +Input text e label diventano: + +L'input text Pallet diventa vuoto e acquisisce il focus +L'input text Cella diventa 9000000 +La label 1 , dall'alto , diventa rossa +Le altre 3 sono vuote e grige. + +Questo stato iniziale è identico per carico e scarico , la discriminante tra le due operazioni è ciò che l'operatore farà dopo essere entrato in questo stato. + +2 prelievo + +Il passaggio 2 è sempre la lettura di un codice udc. Che può essere fatta da barcode oppure da tastiera, se è fatta da barcode la lettura implica uno spostamento del focus sull'input text Cella. Se è fatta da tastiera all'input del 6° carattere il focus salta automaticamente all'inputtext Cella. + +Se ora l'operatore preme "f4 elimina" il pallet corrente viene associato alla cella 9000000 e di fatto prelevato. +Questo chiude il prelievo, il dato viene inviato al server e la 4 label diventano + +Ok scarico - 698345 -> verde +P2506000007 +S-174 +Center ring + +Questo è lo scarico , dopo 2 secondi la form si resetta come in 1. + + +3 versamento + +Se all'atto della lettura del codice udc anzichè lasciare 9000000 nell'input text l'operatore leggesse un codice di cella questo implicherebbe un versamento di quell'udc in quella cella. +La lettura del codice di cella può avvenire solo da barcode e non da tastiera, a meno che l'oepratore non conosca effettivamente quel codice. + +Nel momento in cui l'operatore legge il barcode della cella automaticamente parte l'aggiornamento del versameto sul server. Se l'operazine è andata a buona fine le label intermendi diventano + +OK carico . --> verde +Lotto +codice +descrizione + +dopo 2 secondi dall'ok il form si resetta come in 1. + +4 pickinglkist + +se esiste una picking list con id stato 1 questa può essere prelevata mediante f1, con f2 si salta alla lista successiva con numdoc più basso. Se non c'è nessuna picking list prenotata f1 resetta il barcode alla condizione 1 . Se premo f2 salto alla picking list successiva per numdoc a quella con il numdoc più basso di tutte. In pratica quella con il numdoc più basso può andare in f1 solo se viene prenotata. + +Supponiamo di avere prentato una pickinglist e quindi di premere f1, oppure di andare sulla successiva a quella con il numdoc più basso con f2. Premere f1 o f2 se ci sono plist prenotate o più plist, non perdispone più al prelievo o versamento di cui ai punti 1 e 2 ma allo scorrimento/prelievo di una picking list. + +L'operatore preme f1 , gli inputtext e le label diventano +pallet : vuoto con focus +cella: 9000000 + +Ok Cella indirizzo cella +numdoc della pickinglist +descrizione picking list +num udc + +Questa informazioni sono quelle prese dal contneuto dle picking list +, cioè la prima udc da prelevare, la sua locazione e la descrizone del docuemnto corrente selezionato. +L'operatore va quindi a cercare la cella, il focus si è posizionato su inputtext pallet. +L'operatore legge il barcode della UDC, oppure digita il codice e al sesto carattere scatta il tab sull'input successivo. + +Qui , in automatico, parte il controllo ce codice udc sia quello giusto, se ciè è vero parte il dato per il database che associa l'udc alla cella 7G etc. non ricordo, che significa che l'udc è stata spedita. Cosa avvenga in questo punto è da verificare sul codice + +Se è tutto ok la prima label diviene "ok scarico " -- verde +e occorre vedere sul codice c# cosa compare nelle altre labels. + +Dopo 2 secondi al form si posiziona sulla successiva udc e il ciclo ricomincia. + + + + + + \ No newline at end of file diff --git a/gestione_layout.py b/gestione_layout.py index 54f7532..2d020ac 100644 --- a/gestione_layout.py +++ b/gestione_layout.py @@ -21,6 +21,7 @@ from audit_log import log_user_action from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from gestione_scarico import DEFAULT_SCARICO_USER, move_pallet_async, open_scarico_dialog +from locale_text import load_locale_catalog, text as loc_text from tksheet import Sheet, natural_sort_key from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from ui_theme import theme_color, theme_font, theme_section, theme_value @@ -209,8 +210,9 @@ class LayoutWindow(ctk.CTkToplevel): """Create the window and initialize the state used by the matrix view.""" super().__init__(parent) self._theme = theme_section("layout_window", {}) + self._locale_catalog = load_locale_catalog() self._tooltip_catalog = load_tooltip_catalog() - self.title("Warehouse - Layout corsie") + self.title(loc_text("layout.title", catalog=self._locale_catalog, default="Layout corsie")) self.geometry(str(theme_value(self._theme, "window_geometry", "1200x740"))) minsize = theme_value(self._theme, "window_minsize", [980, 560]) self.minsize(int(minsize[0]), int(minsize[1])) @@ -307,7 +309,7 @@ class LayoutWindow(ctk.CTkToplevel): self.search_entry.grid(row=0, column=0, sticky="w") btn_search = ctk.CTkButton( srch, - text="Cerca per barcode UDC", + text=loc_text("layout.button.search", catalog=self._locale_catalog, default="Cerca per barcode UDC"), command=self._search_udc, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), ) @@ -324,14 +326,14 @@ class LayoutWindow(ctk.CTkToplevel): pass btn_refresh = ctk.CTkButton( tb, - text="Aggiorna", + text=loc_text("layout.button.refresh", catalog=self._locale_catalog, default="Aggiorna"), command=self._refresh_current, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), ) btn_refresh.grid(row=0, column=0, padx=4) btn_export = ctk.CTkButton( tb, - text="Export XLSX", + text=loc_text("layout.button.export", catalog=self._locale_catalog, default="Export XLSX"), command=self._export_xlsx, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), ) @@ -918,13 +920,13 @@ class LayoutWindow(ctk.CTkToplevel): bottom.grid(row=2, column=0, sticky="nsew", padx=8, pady=6) bottom.grid_columnconfigure(0, weight=1) - ctk.CTkLabel(bottom, text="Riempimento globale", font=("", 10, "bold")).grid(row=0, column=0, sticky="w", pady=(0, 2)) + ctk.CTkLabel(bottom, text=loc_text("layout.fill.global", catalog=self._locale_catalog, default="Riempimento globale"), font=("", 10, "bold")).grid(row=0, column=0, sticky="w", pady=(0, 2)) self.tot_canvas = tk.Canvas(bottom, height=22, highlightthickness=0) self.tot_canvas.grid(row=1, column=0, sticky="ew", padx=(0, 260)) self.tot_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0)) self.tot_text.grid(row=1, column=0, sticky="e") - ctk.CTkLabel(bottom, text="Riempimento corsia selezionata", font=("", 10, "bold")).grid(row=2, column=0, sticky="w", pady=(10, 2)) + ctk.CTkLabel(bottom, text=loc_text("layout.fill.selected", catalog=self._locale_catalog, default="Riempimento corsia selezionata"), font=("", 10, "bold")).grid(row=2, column=0, sticky="w", pady=(10, 2)) self.sel_canvas = tk.Canvas(bottom, height=22, highlightthickness=0) self.sel_canvas.grid(row=3, column=0, sticky="ew", padx=(0, 260)) self.sel_text = ctk.CTkLabel(bottom, text=pct_text(0.0, 0.0)) @@ -932,7 +934,7 @@ class LayoutWindow(ctk.CTkToplevel): leg = ctk.CTkFrame(bottom) leg.grid(row=4, column=0, sticky="w", pady=(10, 0)) - ctk.CTkLabel(leg, text="Legenda celle:").grid(row=0, column=0, padx=(0, 8)) + ctk.CTkLabel(leg, text=loc_text("layout.legend", catalog=self._locale_catalog, default="Legenda celle:")).grid(row=0, column=0, padx=(0, 8)) self._legend(leg, 1, "Vuota", COLOR_EMPTY) self._legend(leg, 3, "Piena", COLOR_FULL) self._legend(leg, 5, "Doppia UDC", COLOR_DOUBLE) @@ -1348,7 +1350,13 @@ class LayoutWindow(ctk.CTkToplevel): p_dbl = float(rows[0][1] or 0.0) if rows else 0.0 self._draw_bar(self.tot_canvas, p_full) self.tot_text.configure(text=pct_text(p_full, p_dbl)) - self._async.run(self.db.query_json(sql_tot, {}), _ok, lambda e: None, busy=None, message=None) + self._async.run( + self.db.query_json(sql_tot, {}), + _ok, + lambda e: None, + busy=self._busy, + message="Aggiorno statistiche globali...", + ) # selezionata dalla matrice in memoria if self.matrix_state: diff --git a/gestione_pickinglist.py b/gestione_pickinglist.py index ba00eeb..a546d4c 100644 --- a/gestione_pickinglist.py +++ b/gestione_pickinglist.py @@ -66,7 +66,10 @@ except Exception: natural_sort_key = None # type: ignore[assignment] # Usa overlay e runner "collaudati" -from gestione_aree import BusyOverlay, AsyncRunner +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, theme_value from user_session import UserSession from window_placement import place_window_fullsize_below_parent_later @@ -583,12 +586,18 @@ class GestionePickingListFrame(ctk.CTkFrame): def __init__(self, master, *, db_client=None, conn_str=None, session: UserSession | None = None): """Create the master/detail picking list frame.""" super().__init__(master) + self._theme = theme_section("pickinglist_window", {}) + self._locale_catalog = load_locale_catalog() if db_client is None: raise ValueError("GestionePickingListFrame richiede un db_client condiviso.") self.db_client = db_client self.session = session self.runner = AsyncRunner(self) # runner condiviso (usa loop globale) - self.busy = BusyOverlay(self) # overlay collaudato + self.busy = InlineBusyOverlay(self, self._theme) + try: + self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f"))) + except Exception: + pass self.rows_models: list[PLRow] = [] self._detail_cache: Dict[Any, list] = {} @@ -601,8 +610,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self._render_job = None # Tracking del job di rendering in corso self._build_layout() - # 🔇 Niente reload immediato: carichiamo quando la finestra è idle (= già resa) - self.after_idle(self._first_show) + self._initial_load_started = False def _can(self, action: str) -> bool: """Return whether the current user can execute one picking-list action.""" @@ -616,6 +624,9 @@ class GestionePickingListFrame(ctk.CTkFrame): def _first_show(self): """Chiamato a finestra già resa → evitiamo sfarfallio del primo paint e mostriamo wait-cursor.""" + if self._initial_load_started: + return + self._initial_load_started = True self._first_loading = True try: self.winfo_toplevel().configure(cursor="watch") @@ -633,13 +644,22 @@ class GestionePickingListFrame(ctk.CTkFrame): top = ctk.CTkFrame(self) top.grid(row=0, column=0, sticky="ew", padx=10, pady=(8,4)) + try: + top.configure(fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b"))) + except Exception: + pass for i, (text, cmd) in enumerate([ - ("Ricarica", self.reload_from_db), - ("Prenota", self.on_prenota), - ("S-prenota", self.on_sprenota), - ("Esporta XLSX", self.on_export) + (loc_text("picking.button.reload", catalog=self._locale_catalog, default="Ricarica"), self.reload_from_db), + (loc_text("picking.button.prenota", catalog=self._locale_catalog, default="Prenota"), self.on_prenota), + (loc_text("picking.button.sprenota", catalog=self._locale_catalog, default="S-prenota"), self.on_sprenota), + (loc_text("picking.button.export", catalog=self._locale_catalog, default="Esporta XLSX"), self.on_export) ]): - ctk.CTkButton(top, text=text, command=cmd).grid(row=0, column=i, padx=6) + ctk.CTkButton( + top, + text=text, + command=cmd, + font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")), + ).grid(row=0, column=i, padx=6) # --- micro spinner a destra della toolbar --- self.spinner = ToolbarSpinner(top) @@ -942,7 +962,7 @@ class GestionePickingListFrame(ctk.CTkFrame): # ----- load PL ----- @_log_call() - def reload_from_db(self, first: bool = False): + def reload_from_db(self, first: bool = False, reselect_documento: str | None = None): """Load or reload the picking list summary table from the database.""" self.spinner.start(" Carico…") # spinner ON async def _job(): @@ -952,6 +972,8 @@ class GestionePickingListFrame(ctk.CTkFrame): rows = _rows_to_dicts(res) _log_dataset("SQL_PL", rows) self._refresh_mid_rows(rows) + if reselect_documento: + self.after_idle(lambda doc=reselect_documento: self._reselect_documento_after_reload(doc)) self.spinner.stop() # spinner OFF # se era il primo load, ripristina il cursore standard if self._first_loading: @@ -1057,7 +1079,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self.spinner.start(" Prenoto…") async def _job(): - return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento) + return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento, "P") def _ok(res: SPResult): _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Esito prenotazione documento={documento} rc={getattr(res, 'rc', None)} message={getattr(res, 'message', None)}") @@ -1070,7 +1092,8 @@ class GestionePickingListFrame(ctk.CTkFrame): outcome="ok", target=documento, ) - self._recolor_row_by_documento(documento, desired) + self._detail_cache.pop(documento, None) + self.reload_from_db(reselect_documento=documento) else: msg = (res.message if res else "Errore sconosciuto") log_user_action( @@ -1130,7 +1153,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self.spinner.start(" S-prenoto…") async def _job(): - return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento) + return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento, "S") def _ok(res: SPResult): _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Esito s-prenotazione documento={documento} rc={getattr(res, 'rc', None)} message={getattr(res, 'message', None)}") @@ -1143,7 +1166,8 @@ class GestionePickingListFrame(ctk.CTkFrame): outcome="ok", target=documento, ) - self._recolor_row_by_documento(documento, desired) + self._detail_cache.pop(documento, None) + self.reload_from_db(reselect_documento=documento) else: msg = (res.message if res else "Errore sconosciuto") log_user_action( @@ -1215,9 +1239,12 @@ def open_pickinglist_window(parent: tk.Misc, db_client, session: UserSession | N pass win = ctk.CTkToplevel(parent) - win.title("Gestione Picking List") - win.geometry("1200x700") - win.minsize(1000, 560) + locale_catalog = load_locale_catalog() + win.title(loc_text("picking.title", catalog=locale_catalog, default="Gestione Picking List")) + theme = theme_section("pickinglist_window", {}) + win.geometry(str(theme_value(theme, "window_geometry", "1200x700"))) + minsize = theme_value(theme, "window_minsize", [1000, 560]) + win.minsize(int(minsize[0]), int(minsize[1])) setattr(parent, key, win) # Keep the toplevel hidden until the child frame has built its initial layout. @@ -1238,19 +1265,9 @@ def open_pickinglist_window(parent: tk.Misc, db_client, session: UserSession | N try: win.update_idletasks() place_window_fullsize_below_parent_later(parent, win) - try: - win.deiconify() - except Exception: - pass - win.lift() - try: - win.focus_force() - except Exception: - pass - try: - win.attributes("-alpha", 1.0) - except Exception: - pass + win.after(340, lambda: frame._first_show()) + win.after(360, lambda: win.lift() if getattr(win, "winfo_exists", lambda: False)() else None) + win.after(380, lambda: win.focus_force() if getattr(win, "winfo_exists", lambda: False)() else None) except Exception: pass diff --git a/gestione_scarico.py b/gestione_scarico.py index 067ad06..c005de5 100644 --- a/gestione_scarico.py +++ b/gestione_scarico.py @@ -18,6 +18,8 @@ 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: @@ -449,11 +451,13 @@ class ScaricoDialog(ctk.CTkToplevel): self.on_completed = on_completed self.session = session self.rows: list[ScaricoRow] = [] - self._busy = InlineBusyOverlay(self) + 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(f"Scarica {ubicazione}") + 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) @@ -478,10 +482,19 @@ class ScaricoDialog(ctk.CTkToplevel): 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( + 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="Seleziona le UDC da scaricare", anchor="w").grid( + 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) ) @@ -496,8 +509,8 @@ class ScaricoDialog(ctk.CTkToplevel): 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")) + 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, @@ -507,9 +520,9 @@ class ScaricoDialog(ctk.CTkToplevel): 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.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") @@ -524,10 +537,20 @@ class ScaricoDialog(ctk.CTkToplevel): 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( + 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="close", command=self._close).grid( + 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 ) @@ -606,7 +629,11 @@ class ScaricoDialog(ctk.CTkToplevel): 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) + 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( @@ -622,7 +649,11 @@ class ScaricoDialog(ctk.CTkToplevel): """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) + 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( @@ -693,7 +724,11 @@ class ScaricoDialog(ctk.CTkToplevel): target=self.ubicazione, details={"error": str(ex)}, ) - messagebox.showerror("Scarica", f"Scarico fallito:\n{ex}", parent=self) + 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(), diff --git a/locale.json b/locale.json new file mode 100644 index 0000000..244557f --- /dev/null +++ b/locale.json @@ -0,0 +1,197 @@ +{ + "default_language": "IT", + "IT": { + "launcher.window_title": "Warehouse 1.0.0", + "launcher.operator": "Operatore: {display_name} ({login})", + "launcher.reset_corsie": "Gestione Corsie", + "launcher.layout": "Gestione Layout", + "launcher.multi_udc": "UDC Fantasma", + "launcher.search": "Ricerca UDC", + "launcher.pickinglist": "Gestione Picking List", + "launcher.arrange": "Ridisponi finestre", + "launcher.exit": "Esci", + "launcher.already_running_title": "Warehouse", + "launcher.already_running_message": "L'applicazione è già in esecuzione.", + "login.title": "Login Warehouse", + "login.heading": "Autenticazione operatore", + "login.label.login": "Login", + "login.label.password": "Password", + "login.info": "Per ora tutti gli operatori autenticati possono usare tutte le funzioni.", + "login.button.cancel": "Annulla", + "login.button.submit": "Accedi", + "login.status.checking": "Verifico credenziali...", + "login.msg.title": "Login", + "login.msg.missing": "Inserisci login e password.", + "login.msg.invalid": "Credenziali non valide.", + "login.msg.error": "Verifica credenziali fallita:\n{error}", + "dbconfig.title": "Configurazione Database", + "dbconfig.heading": "Configura la connessione al database del magazzino", + "dbconfig.label.server": "Server", + "dbconfig.label.database": "Database", + "dbconfig.label.user": "Utente", + "dbconfig.label.password": "Password", + "dbconfig.label.driver": "Driver ODBC", + "dbconfig.label.encrypt": "Encrypt", + "dbconfig.label.trust_server_certificate": "Trust server certificate", + "dbconfig.info": "Il file verra' salvato localmente e non verra' piu' richiesto ai prossimi avvii.", + "dbconfig.button.cancel": "Annulla", + "dbconfig.button.test": "Test connessione", + "dbconfig.button.save": "Salva", + "dbconfig.busy": "Verifico connessione...", + "dbconfig.msg.title": "Configurazione Database", + "dbconfig.msg.missing": "Compila almeno server, database, utente e password.", + "dbconfig.msg.test_error": "Connessione fallita:\n{error}", + "dbconfig.msg.test_ok": "Connessione riuscita.", + "dbconfig.msg.save_error": "Salvataggio fallito:\n{error}", + "search.title": "Ricerca UDC/Lotto/Codice", + "search.label.udc": "UDC:", + "search.label.lot": "Lotto:", + "search.label.code": "Codice prodotto:", + "search.button.search": "Cerca", + "search.button.export": "Esporta XLSX", + "search.msg.confirm_title": "Conferma", + "search.msg.confirm_all": "Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?", + "search.msg.export_title": "Esporta", + "search.msg.export_empty": "Non ci sono righe da esportare.", + "search.msg.export_dep": "Per l'esportazione serve 'openpyxl' (pip install openpyxl).", + "search.msg.export_error": "Errore durante l'esportazione:{error}", + "search.msg.no_results_title": "Nessun risultato", + "search.msg.no_results": "Nessuna corrispondenza trovata con le chiavi di ricerca inserite.", + "search.msg.error_title": "Errore ricerca", + "search.busy": "Cerco...", + "reset.title": "Gestione Corsie - svuotamento celle per corsia", + "reset.label.aisle": "Corsia:", + "reset.button.refresh": "Carica", + "reset.button.empty": "Svuota corsia...", + "reset.summary": "Riepilogo", + "layout.title": "Layout corsie", + "layout.button.search": "Cerca per barcode UDC", + "layout.button.refresh": "Aggiorna", + "layout.button.export": "Export XLSX", + "layout.fill.global": "Riempimento globale", + "layout.fill.selected": "Riempimento corsia selezionata", + "layout.legend": "Legenda celle:", + "multi.title": "Celle con piu' pallet", + "multi.button.refresh": "Aggiorna", + "multi.button.expand": "Espandi tutto", + "multi.button.collapse": "Comprimi tutto", + "multi.button.preselect": "Preselezione fantasmi corsia", + "multi.button.remove": "Rimuovi fantasmi corsia", + "multi.button.export": "Esporta in XLSX", + "multi.summary": "Riepilogo % celle multiple per corsia", + "picking.title": "Gestione Picking List", + "picking.button.reload": "Ricarica", + "picking.button.prenota": "Prenota", + "picking.button.sprenota": "S-prenota", + "picking.button.export": "Esporta XLSX", + "scarico.title": "Scarica {ubicazione}", + "scarico.label.location": "Ubicazione: {ubicazione}", + "scarico.label.select": "Seleziona le UDC da scaricare", + "scarico.col.udc": "UDC", + "scarico.col.last_insert": "Ultimo inserimento", + "scarico.col.diagnostic": "Diagnostica", + "scarico.button.submit": "Scarica", + "scarico.button.close": "Chiudi", + "scarico.msg.title": "Scarica", + "scarico.msg.select_one": "Seleziona almeno una UDC da scaricare.", + "scarico.msg.load_error": "Caricamento UDC fallito:\n{error}", + "scarico.msg.exec_error": "Scarico fallito:\n{error}" + }, + "ENG": { + "launcher.window_title": "Warehouse 1.0.0", + "launcher.operator": "Operator: {display_name} ({login})", + "launcher.reset_corsie": "Aisle Management", + "launcher.layout": "Layout Management", + "launcher.multi_udc": "Ghost UDC", + "launcher.search": "UDC Search", + "launcher.pickinglist": "Picking List Management", + "launcher.arrange": "Arrange windows", + "launcher.exit": "Exit", + "launcher.already_running_title": "Warehouse", + "launcher.already_running_message": "The application is already running.", + "login.title": "Warehouse Login", + "login.heading": "Operator authentication", + "login.label.login": "Login", + "login.label.password": "Password", + "login.info": "For now, every authenticated operator can use every function.", + "login.button.cancel": "Cancel", + "login.button.submit": "Sign in", + "login.status.checking": "Checking credentials...", + "login.msg.title": "Login", + "login.msg.missing": "Enter login and password.", + "login.msg.invalid": "Invalid credentials.", + "login.msg.error": "Credential check failed:\n{error}", + "dbconfig.title": "Database Configuration", + "dbconfig.heading": "Configure the warehouse database connection", + "dbconfig.label.server": "Server", + "dbconfig.label.database": "Database", + "dbconfig.label.user": "User", + "dbconfig.label.password": "Password", + "dbconfig.label.driver": "ODBC Driver", + "dbconfig.label.encrypt": "Encrypt", + "dbconfig.label.trust_server_certificate": "Trust server certificate", + "dbconfig.info": "The file will be saved locally and will not be requested again on the next startup.", + "dbconfig.button.cancel": "Cancel", + "dbconfig.button.test": "Test connection", + "dbconfig.button.save": "Save", + "dbconfig.busy": "Checking connection...", + "dbconfig.msg.title": "Database Configuration", + "dbconfig.msg.missing": "Fill in at least server, database, user and password.", + "dbconfig.msg.test_error": "Connection failed:\n{error}", + "dbconfig.msg.test_ok": "Connection successful.", + "dbconfig.msg.save_error": "Save failed:\n{error}", + "search.title": "Search UDC/Lot/Code", + "search.label.udc": "UDC:", + "search.label.lot": "Lot:", + "search.label.code": "Product code:", + "search.button.search": "Search", + "search.button.export": "Export XLSX", + "search.msg.confirm_title": "Confirm", + "search.msg.confirm_all": "No filters set. Search the whole warehouse?", + "search.msg.export_title": "Export", + "search.msg.export_empty": "There are no rows to export.", + "search.msg.export_dep": "Export requires 'openpyxl' (pip install openpyxl).", + "search.msg.export_error": "Export failed:{error}", + "search.msg.no_results_title": "No results", + "search.msg.no_results": "No matches were found for the entered search keys.", + "search.msg.error_title": "Search error", + "search.busy": "Searching...", + "reset.title": "Aisle Management - empty cells by aisle", + "reset.label.aisle": "Aisle:", + "reset.button.refresh": "Load", + "reset.button.empty": "Empty aisle...", + "reset.summary": "Summary", + "layout.title": "Aisle layout", + "layout.button.search": "Search by UDC barcode", + "layout.button.refresh": "Refresh", + "layout.button.export": "Export XLSX", + "layout.fill.global": "Global occupancy", + "layout.fill.selected": "Selected aisle occupancy", + "layout.legend": "Cell legend:", + "multi.title": "Cells with multiple pallets", + "multi.button.refresh": "Refresh", + "multi.button.expand": "Expand all", + "multi.button.collapse": "Collapse all", + "multi.button.preselect": "Preselect aisle ghosts", + "multi.button.remove": "Remove aisle ghosts", + "multi.button.export": "Export to XLSX", + "multi.summary": "Summary % of multi-UDC cells by aisle", + "picking.title": "Picking List Management", + "picking.button.reload": "Reload", + "picking.button.prenota": "Reserve", + "picking.button.sprenota": "Unreserve", + "picking.button.export": "Export XLSX", + "scarico.title": "Unload {ubicazione}", + "scarico.label.location": "Location: {ubicazione}", + "scarico.label.select": "Select the UDCs to unload", + "scarico.col.udc": "UDC", + "scarico.col.last_insert": "Last insert", + "scarico.col.diagnostic": "Diagnostics", + "scarico.button.submit": "Unload", + "scarico.button.close": "Close", + "scarico.msg.title": "Unload", + "scarico.msg.select_one": "Select at least one UDC to unload.", + "scarico.msg.load_error": "UDC load failed:\n{error}", + "scarico.msg.exec_error": "Unload failed:\n{error}" + } +} diff --git a/locale_text.py b/locale_text.py new file mode 100644 index 0000000..d68bac9 --- /dev/null +++ b/locale_text.py @@ -0,0 +1,41 @@ +"""Localized UI text loader for the warehouse desktop application.""" + +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path + + +_LOCALE_FILE = Path(__file__).with_name("locale.json") + + +@lru_cache(maxsize=1) +def load_locale_catalog() -> dict: + """Load the locale catalog from JSON, returning a safe default on errors.""" + + try: + return json.loads(_LOCALE_FILE.read_text(encoding="utf-8")) + except Exception: + return {"default_language": "IT", "IT": {}, "ENG": {}} + + +def reload_locale_catalog() -> dict: + """Clear the locale cache and reload the catalog from disk.""" + + load_locale_catalog.cache_clear() + return load_locale_catalog() + + +def text(key: str, *, language: str | None = None, catalog: dict | None = None, default: str = "") -> str: + """Return the localized UI text for ``key`` with Italian fallback.""" + + data = catalog or load_locale_catalog() + lang = str(language or data.get("default_language") or "IT").upper() + texts = data.get(lang, {}) or {} + if key in texts: + return str(texts[key]) + fallback = data.get("IT", {}) or {} + if key in fallback: + return str(fallback[key]) + return str(default) diff --git a/login_window.py b/login_window.py index 40a4a7e..1f0c946 100644 --- a/login_window.py +++ b/login_window.py @@ -7,7 +7,10 @@ from tkinter import messagebox, ttk from typing import Any from audit_log import log_session_event +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_section, theme_value from user_session import UserSession, create_user_session @@ -48,17 +51,21 @@ def _rows_to_dicts(res: Any) -> list[dict[str, Any]]: class LoginWindow(tk.Toplevel): """Small modal dialog used to authenticate one warehouse operator.""" - def __init__(self, parent: tk.Misc, db_client): + def __init__(self, parent: tk.Misc, db_client, *, compact: bool = False): super().__init__(parent) self.db_client = db_client + self.compact = bool(compact) self.result_session: UserSession | None = None self._async = AsyncRunner(self) + self._theme = theme_section("login_window", {}) + self._locale_catalog = load_locale_catalog() self._login_button: ttk.Button | None = None self._cancel_button: ttk.Button | None = None self._status_var = tk.StringVar(value="") + self._busy = InlineBusyOverlay(self, self._theme) - self.title("Login Warehouse") - self.geometry("420x250") + self.title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login")) + self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "420x250"))) self.resizable(False, False) try: if parent is not None and parent.winfo_viewable(): @@ -81,45 +88,85 @@ class LoginWindow(tk.Toplevel): def _build_ui(self) -> None: """Build the compact operator login form.""" - body = ttk.Frame(self, padding=12) + body = ttk.Frame(self, padding=8 if self.compact else 12) body.pack(fill="both", expand=True) body.columnconfigure(1, weight=1) - ttk.Label( - body, - text="Autenticazione operatore", - font=("Segoe UI", 11, "bold"), - ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(4, 14)) + row_offset = 0 + if not self.compact: + ttk.Label( + body, + text=loc_text("login.heading", catalog=self._locale_catalog, default="Autenticazione operatore"), + font=("Segoe UI", 11, "bold"), + ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(4, 14)) + row_offset = 1 - ttk.Label(body, text="Login").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6) - self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=28) - self.login_entry.grid(row=1, column=1, sticky="ew", pady=6) - - ttk.Label(body, text="Password").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=6) - self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=28, show="*") - self.password_entry.grid(row=2, column=1, sticky="ew", pady=6) - - self.info_label = ttk.Label( - body, - text="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.", - justify="left", - wraplength=320, + ttk.Label(body, text="User:").grid( + row=row_offset, column=0, sticky="w", padx=(0, 6), pady=4 if self.compact else 6 ) - self.info_label.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 8)) + self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9)) + self.login_entry.grid(row=row_offset, column=1, sticky="ew", pady=4 if self.compact else 6) - self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") - self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(2, 2)) + ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 6), pady=4 if self.compact else 6) + self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9)) + self.password_entry.grid(row=row_offset + 1, column=1, sticky="ew", pady=4 if self.compact else 6) - actions = ttk.Frame(body) - actions.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(6, 0)) - actions.columnconfigure(0, weight=1) - self._cancel_button = ttk.Button(actions, text="Annulla", command=self._on_cancel) - self._cancel_button.grid(row=0, column=1, padx=(0, 8), pady=8) - self._login_button = ttk.Button(actions, text="Accedi", command=self._on_login) - self._login_button.grid(row=0, column=2, pady=8) + if self.compact: + actions = ttk.Frame(body) + actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0)) + self._cancel_button = ttk.Button( + actions, + text="Annulla", + command=self._on_cancel, + ) + self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0)) + self._login_button = ttk.Button( + actions, + text="OK", + command=self._on_login, + ) + self._login_button.grid(row=0, column=0, sticky="ew") + else: + self.info_label = ttk.Label( + body, + text=loc_text( + "login.info", + catalog=self._locale_catalog, + default="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.", + ), + justify="left", + wraplength=320, + ) + self.info_label.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 8)) + + self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") + self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(2, 2)) + + actions = ttk.Frame(body) + actions.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(6, 0)) + actions.columnconfigure(0, weight=1) + self._cancel_button = ttk.Button( + actions, + text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"), + command=self._on_cancel, + ) + self._cancel_button.grid(row=0, column=1, padx=(0, 8), pady=8) + self._login_button = ttk.Button( + actions, + text=loc_text("login.button.submit", catalog=self._locale_catalog, default="Accedi"), + command=self._on_login, + ) + self._login_button.grid(row=0, column=2, pady=8) self.bind("", lambda _e: self._on_login()) self.bind("", lambda _e: self._on_cancel()) + self.login_var.trace_add("write", lambda *_: self._limit_var(self.login_var, 10)) + self.password_var.trace_add("write", lambda *_: self._limit_var(self.password_var, 10)) + + def _limit_var(self, variable: tk.StringVar, max_len: int) -> None: + value = str(variable.get() or "") + if len(value) > max_len: + variable.set(value[:max_len]) def _set_busy(self, busy: bool, message: str = "") -> None: """Enable or disable user interaction during async authentication.""" @@ -134,6 +181,10 @@ class LoginWindow(tk.Toplevel): self._cancel_button.configure(state=state) self.configure(cursor="watch" if busy else "") self._status_var.set(message) + if busy: + self._busy.show(message or loc_text("login.status.checking", catalog=self._locale_catalog, default="Verifico credenziali...")) + else: + self._busy.hide() self.update_idletasks() except Exception: pass @@ -167,11 +218,15 @@ class LoginWindow(tk.Toplevel): login = str(self.login_var.get() or "").strip() password = str(self.password_var.get() or "").strip() if not login or not password: - messagebox.showwarning("Login", "Inserisci login e password.", parent=self) + messagebox.showwarning( + loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), + loc_text("login.msg.missing", catalog=self._locale_catalog, default="Inserisci login e password."), + parent=self, + ) return params = {"login": login, "password": password} - self._set_busy(True, "Verifico credenziali...") + self._set_busy(True, loc_text("login.status.checking", catalog=self._locale_catalog, default="Verifico credenziali...")) def _ok(res: Any) -> None: rows = _rows_to_dicts(res) @@ -183,7 +238,11 @@ class LoginWindow(tk.Toplevel): outcome="denied", details={"login": login}, ) - messagebox.showerror("Login", "Credenziali non valide.", parent=self) + messagebox.showerror( + loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), + loc_text("login.msg.invalid", catalog=self._locale_catalog, default="Credenziali non valide."), + parent=self, + ) try: self.password_var.set("") self.password_entry.focus_force() @@ -215,7 +274,11 @@ class LoginWindow(tk.Toplevel): outcome="error", details={"login": login, "error": str(ex)}, ) - messagebox.showerror("Login", f"Verifica credenziali fallita:\n{ex}", parent=self) + messagebox.showerror( + loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), + loc_text("login.msg.error", catalog=self._locale_catalog, default="Verifica credenziali fallita:\n{error}").format(error=ex), + parent=self, + ) self._async.run( self.db_client.query_json(SQL_LOGIN, params), @@ -249,3 +312,11 @@ def prompt_login(parent: tk.Misc, db_client) -> UserSession | None: dialog = LoginWindow(parent, db_client) dialog.wait_window() return dialog.result_session + + +def prompt_login_compact(parent: tk.Misc, db_client) -> UserSession | None: + """Open the barcode-oriented compact login modal.""" + + dialog = LoginWindow(parent, db_client, compact=True) + dialog.wait_window() + return dialog.result_session diff --git a/main.py b/main.py index 9f51eca..3c236bd 100644 --- a/main.py +++ b/main.py @@ -15,29 +15,28 @@ import customtkinter as ctk from tkinter import messagebox from async_loop_singleton import get_global_loop -from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn +from async_msssql_query import AsyncMSSQLClient from audit_log import log_session_event +from db_config import build_dsn_from_config, ensure_db_config from gestione_layout import open_layout_window from gestione_pickinglist import open_pickinglist_window from login_window import prompt_login +from locale_text import load_locale_catalog, text as loc_text from reset_corsie import open_reset_corsie_window from search_pallets import open_search_window from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from ui_theme import theme_font, theme_section, theme_value from user_session import UserSession, create_user_session from view_celle_multi_udc import open_celle_multiple_window -from window_placement import cascade_children_below_parent, place_window_fullsize_below_parent_later - - -# ---- Config ---- -SERVER = r"mde3\gesterp" -DBNAME = "Mediseawall" -USER = "sa" -PASSWORD = "1Password1" +from window_placement import ( + cascade_children_below_parent, + place_window_below_parent_later, + place_window_fullsize_below_parent_later, +) # Development shortcut: skip the login dialog and boot directly as MAG1. # Set to False when you want to restore normal authentication. -BYPASS_LOGIN = True +BYPASS_LOGIN = False BYPASS_LOGIN_USER = { "operator_id": 4, "login": "MAG1", @@ -67,8 +66,7 @@ if not hasattr(tk.Toplevel, "block_update_dimensions_event"): if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"): tk.Toplevel.unblock_update_dimensions_event = _noop # type: ignore[attr-defined] -dsn_app = make_mssql_dsn(server=SERVER, database=DBNAME, user=USER, password=PASSWORD) -db_app = AsyncMSSQLClient(dsn_app) +db_app: AsyncMSSQLClient | None = None _APP_MUTEX = None _APP_MUTEX_NAME = "Local\\WarehousePythonAppSingleton" @@ -114,18 +112,22 @@ class Launcher(ctk.CTk): "pickinglist", ] - def __init__(self, session: UserSession): + def __init__(self, session: UserSession, db_client: AsyncMSSQLClient): """Create the launcher toolbar and wire every button to a feature window.""" super().__init__() self.session: UserSession = session + self.db_client = db_client self._theme = theme_section("launcher", {}) + self._locale_catalog = load_locale_catalog() self._tooltip_catalog = load_tooltip_catalog() self._child_windows: list[tk.Misc] = [] self._child_windows_by_key: dict[str, tk.Misc] = {} self._is_cascading = False self._focus_restore_pending: set[str] = set() self._restore_suppressed_until = 0.0 - self.title(f"Warehouse 1.0.0 - {self.session.display_name}") + self.title( + f"{loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse 1.0.0')} - {self.session.display_name}" + ) self._apply_dynamic_geometry() outer_pady = int(theme_value(self._theme, "outer_pady", 10)) @@ -142,58 +144,58 @@ class Launcher(ctk.CTk): actions = [ ( "reset_corsie", - "Gestione Corsie", + loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"), "launcher.open_reset_corsie", lambda: self._open_child_window( "reset_corsie", - open_reset_corsie_window(self, db_app, session=self.session), + open_reset_corsie_window(self, self.db_client, session=self.session), ), ), ( "layout", - "Gestione Layout", + loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"), "launcher.open_layout", lambda: self._open_child_window( "layout", - open_layout_window(self, db_app, session=self.session), + open_layout_window(self, self.db_client, session=self.session), ), ), ( "multi_udc", - "UDC Fantasma", + loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"), "launcher.open_multi_udc", lambda: self._open_child_window( "multi_udc", - open_celle_multiple_window(self, db_app, session=self.session), + open_celle_multiple_window(self, self.db_client, session=self.session), ), ), ( "search", - "Ricerca UDC", + loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"), "launcher.open_search", lambda: self._open_child_window( "search", - open_search_window(self, db_app, session=self.session), + open_search_window(self, self.db_client, session=self.session), ), ), ( "pickinglist", - "Gestione Picking List", + loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"), "launcher.open_pickinglist", lambda: self._open_child_window( "pickinglist", - open_pickinglist_window(self, db_app, session=self.session), + open_pickinglist_window(self, self.db_client, session=self.session), ), ), ( "arrange", - "Ridisponi finestre", + loc_text("launcher.arrange", catalog=self._locale_catalog, default="Ridisponi finestre"), "launcher.arrange_windows", self._cascade_open_windows, ), ( "exit", - "Esci", + loc_text("launcher.exit", catalog=self._locale_catalog, default="Esci"), "launcher.exit", self._shutdown, ), @@ -203,7 +205,11 @@ class Launcher(ctk.CTk): info = ctk.CTkLabel( wrap, - text=f"Operatore: {self.session.display_name} ({self.session.login})", + text=loc_text( + "launcher.operator", + catalog=self._locale_catalog, + default="Operatore: {display_name} ({login})", + ).format(display_name=self.session.display_name, login=self.session.login), anchor="w", font=theme_font(self._theme, "info_font", default=("Segoe UI", 12, "bold")), ) @@ -288,18 +294,6 @@ class Launcher(ctk.CTk): ) except Exception: pass - try: - window.bind( - "", - lambda event, win=window, win_key=key: self._on_child_activate(win_key, win, event.widget), - add="+", - ) - except Exception: - pass - try: - window.lift() - except Exception: - pass def _forget_child_window(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None: """Remove stale references to child windows that have been closed.""" @@ -344,7 +338,7 @@ class Launcher(ctk.CTk): return if time.monotonic() < self._restore_suppressed_until: return - if not self._widget_belongs_to_window(window, event_widget): + if event_widget is not None and event_widget is not window: return tracked = self._child_windows_by_key.get(key) if tracked is not window or not getattr(window, "winfo_exists", lambda: False)(): @@ -357,7 +351,11 @@ class Launcher(ctk.CTk): try: tracked_now = self._child_windows_by_key.get(key) if tracked_now is window and getattr(window, "winfo_exists", lambda: False)(): - place_window_fullsize_below_parent_later(self, window) + place_window_below_parent_later(self, window) + try: + window.lift() + except Exception: + pass finally: try: self.after(250, lambda: self._focus_restore_pending.discard(key)) @@ -396,6 +394,7 @@ class Launcher(ctk.CTk): ) def _finish_cascade() -> None: self._is_cascading = False + self._restore_suppressed_until = 0.0 try: self.lift() self.focus_force() @@ -412,11 +411,12 @@ class Launcher(ctk.CTk): try: if self.session is not None: log_session_event(self.session, action="logout", outcome="ok") - fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) - try: - fut.result(timeout=2) - except Exception: - pass + if self.db_client is not None: + fut = asyncio.run_coroutine_threadsafe(self.db_client.dispose(), _loop) + try: + fut.result(timeout=2) + except Exception: + pass finally: self.destroy() @@ -428,8 +428,11 @@ if __name__ == "__main__": root = tk.Tk() root.withdraw() messagebox.showwarning( - "Warehouse", - "L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.", + loc_text("launcher.already_running_title", default="Warehouse"), + loc_text( + "launcher.already_running_message", + default="L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.", + ), parent=root, ) try: @@ -438,6 +441,12 @@ if __name__ == "__main__": pass raise SystemExit(0) + db_cfg = ensure_db_config(_loop) + if db_cfg is None: + raise SystemExit(0) + + db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg)) + if BYPASS_LOGIN: session = _build_bypass_session() log_session_event( @@ -457,11 +466,12 @@ if __name__ == "__main__": session = prompt_login(bootstrap, db_app) if session is None: - fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) - try: - fut.result(timeout=2) - except Exception: - pass + if db_app is not None: + fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) + try: + fut.result(timeout=2) + except Exception: + pass try: if bootstrap is not None: bootstrap.destroy() @@ -475,4 +485,4 @@ if __name__ == "__main__": except Exception: pass - Launcher(session).mainloop() + Launcher(session, db_app).mainloop() diff --git a/patch_sp_xExePackingListPallet.sql b/patch_sp_xExePackingListPallet.sql new file mode 100644 index 0000000..80c12f9 --- /dev/null +++ b/patch_sp_xExePackingListPallet.sql @@ -0,0 +1,119 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + +CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPallet] + @IDOperatore int, + @Documento varchar(8), + @Azione char(1) = 'P', -- 'P' = Prenota, 'S' = S-prenota + @RC int OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET @RC = 0; + + DECLARE @Nominativo varchar(50); + DECLARE @DocumentoPrenotato bit = 0; + DECLARE @Description varchar(255) = ''; + DECLARE @Message varchar(255) = ''; + DECLARE @IDResult int = 0; + DECLARE @ID int = 0; + DECLARE @Code varchar(64) = @Documento; + + SELECT @Nominativo = [Login] + FROM dbo.Operatori + WHERE ID = @IDOperatore; + + IF @Azione NOT IN ('P', 'S') + BEGIN + SET @RC = -10; + RETURN; + END; + + DECLARE @TargetCelle TABLE ( + IDCella int PRIMARY KEY + ); + + INSERT INTO @TargetCelle (IDCella) + SELECT DISTINCT Cella + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento + AND Cella IS NOT NULL; + + IF NOT EXISTS (SELECT 1 FROM @TargetCelle) + BEGIN + SET @RC = -20; + RETURN; + END; + + IF EXISTS ( + SELECT 1 + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento + AND ISNULL(IDStato, 0) = 1 + ) + BEGIN + SET @DocumentoPrenotato = 1; + END; + + IF @Azione = 'P' + BEGIN + -- Gia' prenotata: nessuna variazione + IF @DocumentoPrenotato = 1 + RETURN; + + -- Azzera la prenotazione di tutti gli altri documenti + UPDATE c + SET c.IDStato = 0, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + WHERE c.ID IN ( + SELECT DISTINCT Cella + FROM dbo.XMag_ViewPackingList + WHERE ISNULL(IDStato, 0) = 1 + AND Documento <> @Documento + AND Cella IS NOT NULL + ); + + -- Prenota soltanto le celle del documento target + UPDATE c + SET c.IDStato = 1, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + INNER JOIN @TargetCelle t ON t.IDCella = c.ID; + + SELECT TOP 1 @Description = NAZIONE + FROM dbo.XMag_ViewPackingList + WHERE Documento = @Documento; + + EXECUTE @RC = [dbo].[sp_LogPackingList] + @ID + ,@Code + ,@Description + ,@Message OUTPUT + ,@IDResult OUTPUT; + + RETURN; + END; + + IF @Azione = 'S' + BEGIN + -- Gia' non prenotata: nessuna variazione + IF @DocumentoPrenotato = 0 + RETURN; + + -- Togli la prenotazione soltanto al documento target + UPDATE c + SET c.IDStato = 0, + c.ModUtente = @Nominativo, + c.ModDataOra = GETDATE() + FROM dbo.Celle c + INNER JOIN @TargetCelle t ON t.IDCella = c.ID; + + RETURN; + END; +END; +GO diff --git a/prenota_sprenota_sql.py b/prenota_sprenota_sql.py index 1c109b3..302aa2a 100644 --- a/prenota_sprenota_sql.py +++ b/prenota_sprenota_sql.py @@ -254,92 +254,49 @@ async def _execute(db, sql: str, params: Dict[str, Any]) -> int: @_log_call() -async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) -> SPResult: - """Toggle the reservation state of all cells belonging to a packing list. - - The implementation mirrors the original SQL stored procedure while using - the shared async DB client already managed by the application. - """ +async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult: + """Execute the original reservation stored procedure used by the C# client.""" try: - _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura async packing list avviata documento={Documento} id_operatore={IDOperatore}") - nominativo = await _query_one_value( - db, - "SELECT LOGIN FROM Operatori WHERE id = :IDOperatore", - {"IDOperatore": IDOperatore}, - ) or "" - - celle = await _query_all( - db, - """ - SELECT DISTINCT Cella - FROM dbo.XMag_ViewPackingList - WHERE Documento = :Documento - """, - {"Documento": Documento}, + azione = str(Azione or "P").strip().upper() + if azione not in ("P", "S"): + return SPResult(rc=-10, message=f"Azione non valida: {Azione}", id_result=None) + _MODULE_LOGGER.log( + _MODULE_LOG_LEVEL, + f"Procedura packing list via stored procedure documento={Documento} azione={azione} id_operatore={IDOperatore}", ) - id_celle = [row.get("Cella") for row in celle if "Cella" in row] - _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Celle coinvolte per documento={Documento}: {len(id_celle)}") + sql = """ + SET NOCOUNT ON; + DECLARE @RC int = 0; - # Each cell is toggled individually because the original procedure also - # updates metadata such as operator and timestamp per row. - for id_cella in id_celle: - if id_cella is None: - continue - stato = await _query_one_value( - db, - "SELECT IDStato FROM Celle WHERE ID = :IDC", - {"IDC": id_cella}, - ) - _MODULE_LOGGER.debug(f"Toggling cella id={id_cella} stato_corrente={stato}") - if stato == 0: - await _execute( - db, - """ - UPDATE Celle - SET IDStato = 1, - ModUtente = :N, - ModDataOra = GETDATE() - WHERE ID = :IDC - """, - {"N": nominativo, "IDC": id_cella}, - ) - else: - await _execute( - db, - """ - UPDATE Celle - SET IDStato = 0, - ModUtente = :N, - ModDataOra = GETDATE() - WHERE ID = :IDC - """, - {"N": nominativo, "IDC": id_cella}, - ) + EXEC dbo.sp_xExePackingListPallet + @IDOperatore = :IDOperatore, + @Documento = :Documento, + @Azione = :Azione, + @RC = @RC OUTPUT; - description = await _query_one_value( - db, - """ - SELECT TOP 1 NAZIONE - FROM dbo.XMag_ViewPackingList - WHERE Documento = :Documento - GROUP BY Documento, NAZIONE - ORDER BY NAZIONE - """, - {"Documento": Documento}, + SELECT CAST(@RC AS int) AS RC; + """ + _log_sql("sp_xExePackingListPallet", sql, {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione}) + if not hasattr(db, "query_json"): + raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.") + res = await db.query_json( + sql, + {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione}, + as_dict_rows=True, + commit=True, ) - - await _execute( - db, - """ - INSERT INTO dbo.LogPackingList (Code, Description, IDInsUser, InsDateTime) - VALUES (:Code, :Descr, :IDInsUser, GETDATE()); - """, - {"Code": Documento, "Descr": description, "IDInsUser": IDOperatore}, - ) - - new_id = await _query_one_value(db, "SELECT SCOPE_IDENTITY() AS ID", {}) - _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura completata documento={Documento} id_result={new_id}") - return SPResult(rc=0, message="", id_result=int(new_id) if new_id is not None else None) + rows = [] + if isinstance(res, dict): + rows = res.get("rows", []) or [] + _log_dataset("sp_xExePackingListPallet", rows) + rc = 0 + if rows and isinstance(rows[0], dict): + try: + rc = int(rows[0].get("RC") or 0) + except Exception: + rc = 0 + _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Stored procedure completata documento={Documento} azione={azione} rc={rc}") + return SPResult(rc=rc, message="", id_result=None) except Exception as exc: - _MODULE_LOGGER.exception(f"Procedura fallita documento={Documento}: {exc}") + _MODULE_LOGGER.exception(f"Procedura fallita documento={Documento} azione={Azione}: {exc}") return SPResult(rc=-1, message=str(exc), id_result=None) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7758d53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +aioodbc +customtkinter +loguru +openpyxl +orjson +pyodbc +SQLAlchemy +tksheet diff --git a/reset_corsie.py b/reset_corsie.py index 07ba5fd..7e3dbca 100644 --- a/reset_corsie.py +++ b/reset_corsie.py @@ -21,6 +21,7 @@ import customtkinter as ctk from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from gestione_scarico import move_pallet_async +from locale_text import load_locale_catalog, text as loc_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from ui_theme import theme_color, theme_font, theme_padding, theme_section, theme_value from window_placement import place_window_fullsize_below_parent_later @@ -273,7 +274,8 @@ class ResetCorsieWindow(ctk.CTkToplevel): """Create the window and immediately load the list of aisles.""" super().__init__(parent) self._theme = theme_section("reset_corsie", {}) - self.title("Reset Corsie - svuotamento celle per corsia") + self._locale_catalog = load_locale_catalog() + self.title(loc_text("reset.title", catalog=self._locale_catalog, default="Gestione Corsie - svuotamento celle per corsia")) self.geometry(str(theme_value(self._theme, "window_geometry", "1000x680"))) minsize = theme_value(self._theme, "window_minsize", [880, 560]) self.minsize(int(minsize[0]), int(minsize[1])) @@ -341,7 +343,7 @@ class ResetCorsieWindow(ctk.CTkToplevel): pass ctk.CTkLabel( top, - text="Corsia:", + text=loc_text("reset.label.aisle", catalog=self._locale_catalog, default="Corsia:"), font=theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)), ).pack(side="left") self.cmb = ctk.CTkComboBox( @@ -355,7 +357,7 @@ class ResetCorsieWindow(ctk.CTkToplevel): self.cmb.pack(side="left", padx=(6, 10)) btn_refresh = ctk.CTkButton( top, - text="Carica", + text=loc_text("reset.button.refresh", catalog=self._locale_catalog, default="Carica"), command=self.refresh, width=int(theme_value(self._theme, "toolbar_button_width", 140)), height=int(theme_value(self._theme, "toolbar_button_height", 28)), @@ -365,7 +367,7 @@ class ResetCorsieWindow(ctk.CTkToplevel): btn_refresh.pack(side="left") btn_reset = ctk.CTkButton( top, - text="Svuota corsia...", + text=loc_text("reset.button.empty", catalog=self._locale_catalog, default="Svuota corsia..."), command=self._ask_reset, width=int(theme_value(self._theme, "toolbar_button_width", 140)), height=int(theme_value(self._theme, "toolbar_button_height", 28)), @@ -431,7 +433,7 @@ class ResetCorsieWindow(ctk.CTkToplevel): pass ctk.CTkLabel( bottom, - text="Riepilogo", + text=loc_text("reset.summary", catalog=self._locale_catalog, default="Riepilogo"), font=theme_font(self._theme, "summary_title_font", ("Segoe UI", 12, "bold")), ).pack(anchor="w", padx=8, pady=(8, 0)) diff --git a/search_pallets.py b/search_pallets.py index f6d47fa..cf04d46 100644 --- a/search_pallets.py +++ b/search_pallets.py @@ -7,8 +7,12 @@ from tkinter import filedialog, messagebox, ttk import customtkinter as ctk -from gestione_aree import AsyncRunner, BusyOverlay +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, theme_value from window_placement import place_window_fullsize_below_parent_later +from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text try: from openpyxl import Workbook @@ -78,14 +82,22 @@ class SearchWindow(ctk.CTkToplevel): def __init__(self, parent: tk.Widget, db_app, session=None): """Initialize widgets and keep a reference to the shared DB client.""" super().__init__(parent) - self.title("Warehouse - Ricerca UDC/Lotto/Codice") - self.geometry("1100x720") - self.minsize(900, 560) + self._theme = theme_section("search_window", {}) + self._locale_catalog = load_locale_catalog() + self._tooltip_catalog = load_tooltip_catalog() + self.title(loc_text("search.title", catalog=self._locale_catalog, default="Ricerca UDC/Lotto/Codice")) + self.geometry(str(theme_value(self._theme, "window_geometry", "1100x720"))) + minsize = theme_value(self._theme, "window_minsize", [900, 560]) + self.minsize(int(minsize[0]), int(minsize[1])) self.resizable(True, True) self.db = db_app self.session = session - self._busy = BusyOverlay(self) + try: + self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f"))) + except Exception: + pass + self._busy = InlineBusyOverlay(self, self._theme) self._async = AsyncRunner(self) self._sort_state: dict[str, bool] = {} @@ -97,26 +109,62 @@ class SearchWindow(ctk.CTkToplevel): self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) - top = ctk.CTkFrame(self) + top = ctk.CTkFrame( + self, + fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")), + ) top.grid(row=0, column=0, sticky="nsew", padx=8, pady=8) for i in range(8): top.grid_columnconfigure(i, weight=0) top.grid_columnconfigure(7, weight=1) - ctk.CTkLabel(top, text="UDC:").grid(row=0, column=0, sticky="w") + label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)) + entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10)) + button_font = theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")) + + ctk.CTkLabel( + top, text=loc_text("search.label.udc", catalog=self._locale_catalog, default="UDC:"), font=label_font + ).grid(row=0, column=0, sticky="w") self.var_udc = tk.StringVar() - ctk.CTkEntry(top, textvariable=self.var_udc, width=160).grid(row=0, column=1, sticky="w", padx=(4, 12)) + ctk.CTkEntry(top, textvariable=self.var_udc, width=160, font=entry_font).grid( + row=0, column=1, sticky="w", padx=(4, 12) + ) - ctk.CTkLabel(top, text="Lotto:").grid(row=0, column=2, sticky="w") + ctk.CTkLabel( + top, text=loc_text("search.label.lot", catalog=self._locale_catalog, default="Lotto:"), font=label_font + ).grid(row=0, column=2, sticky="w") self.var_lotto = tk.StringVar() - ctk.CTkEntry(top, textvariable=self.var_lotto, width=140).grid(row=0, column=3, sticky="w", padx=(4, 12)) + ctk.CTkEntry(top, textvariable=self.var_lotto, width=140, font=entry_font).grid( + row=0, column=3, sticky="w", padx=(4, 12) + ) - ctk.CTkLabel(top, text="Codice prodotto:").grid(row=0, column=4, sticky="w") + ctk.CTkLabel( + top, + text=loc_text("search.label.code", catalog=self._locale_catalog, default="Codice prodotto:"), + font=label_font, + ).grid(row=0, column=4, sticky="w") self.var_codice = tk.StringVar() - ctk.CTkEntry(top, textvariable=self.var_codice, width=160).grid(row=0, column=5, sticky="w", padx=(4, 12)) + ctk.CTkEntry(top, textvariable=self.var_codice, width=160, font=entry_font).grid( + row=0, column=5, sticky="w", padx=(4, 12) + ) - ctk.CTkButton(top, text="Cerca", command=self._do_search).grid(row=0, column=6, sticky="w") - ctk.CTkButton(top, text="Esporta XLSX", command=self._export_xlsx).grid(row=0, column=7, sticky="e") + btn_search = ctk.CTkButton( + top, + text=loc_text("search.button.search", catalog=self._locale_catalog, default="Cerca"), + command=self._do_search, + font=button_font, + ) + btn_search.grid(row=0, column=6, sticky="w") + btn_export = ctk.CTkButton( + top, + text=loc_text("search.button.export", catalog=self._locale_catalog, default="Esporta XLSX"), + command=self._export_xlsx, + font=button_font, + ) + btn_export.grid(row=0, column=7, sticky="e") + tip = tooltip_text("launcher.open_search", catalog=self._tooltip_catalog) + if tip: + WidgetToolTip(btn_search, tip) wrap = ctk.CTkFrame(self) wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8)) @@ -168,10 +216,22 @@ class SearchWindow(ctk.CTkToplevel): """Export the currently visible search results to an Excel file.""" rows = [self.tree.item(iid, "values") for iid in self.tree.get_children("")] if not rows: - messagebox.showinfo("Esporta", "Non ci sono righe da esportare.", parent=self) + messagebox.showinfo( + loc_text("search.msg.export_title", catalog=self._locale_catalog, default="Esporta"), + loc_text("search.msg.export_empty", catalog=self._locale_catalog, default="Non ci sono righe da esportare."), + parent=self, + ) return if not _HAS_XLSX: - messagebox.showerror("Esporta", "Per l'esportazione serve 'openpyxl' (pip install openpyxl).", parent=self) + messagebox.showerror( + loc_text("search.msg.export_title", catalog=self._locale_catalog, default="Esporta"), + loc_text( + "search.msg.export_dep", + catalog=self._locale_catalog, + default="Per l'esportazione serve 'openpyxl' (pip install openpyxl).", + ), + parent=self, + ) return from datetime import datetime @@ -211,9 +271,17 @@ class SearchWindow(ctk.CTkToplevel): for j, width in widths.items(): ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60) wb.save(fname) - messagebox.showinfo("Esporta", f"File creato:\n{fname}", parent=self) + messagebox.showinfo( + loc_text("search.msg.export_title", catalog=self._locale_catalog, default="Esporta"), + f"File creato:\n{fname}", + parent=self, + ) except Exception as ex: - messagebox.showerror("Esporta", f"Errore durante l'esportazione:{ex}", parent=self) + messagebox.showerror( + loc_text("search.msg.export_title", catalog=self._locale_catalog, default="Esporta"), + loc_text("search.msg.export_error", catalog=self._locale_catalog, default="Errore durante l'esportazione:{error}").format(error=ex), + parent=self, + ) def _on_dclick(self, evt): """Copy the selected pallet barcode when a result cell is double-clicked.""" @@ -357,8 +425,12 @@ class SearchWindow(ctk.CTkToplevel): if not (udc or lotto or codice): if not messagebox.askyesno( - "Conferma", - "Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?", + loc_text("search.msg.confirm_title", catalog=self._locale_catalog, default="Conferma"), + loc_text( + "search.msg.confirm_all", + catalog=self._locale_catalog, + default="Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?", + ), parent=self, ): return @@ -396,8 +468,12 @@ class SearchWindow(ctk.CTkToplevel): if not rows: messagebox.showinfo( - "Nessun risultato", - "Nessuna corrispondenza trovata con le chiavi di ricerca inserite.", + loc_text("search.msg.no_results_title", catalog=self._locale_catalog, default="Nessun risultato"), + loc_text( + "search.msg.no_results", + catalog=self._locale_catalog, + default="Nessuna corrispondenza trovata con le chiavi di ricerca inserite.", + ), parent=self, ) else: @@ -408,9 +484,19 @@ class SearchWindow(ctk.CTkToplevel): def _err(ex): self._busy.hide() - messagebox.showerror("Errore ricerca", str(ex), parent=self) + messagebox.showerror( + loc_text("search.msg.error_title", catalog=self._locale_catalog, default="Errore ricerca"), + str(ex), + parent=self, + ) - self._async.run(self.db.query_json(SQL_SEARCH, params), _ok, _err, busy=self._busy, message="Cerco...") + self._async.run( + self.db.query_json(SQL_SEARCH, params), + _ok, + _err, + busy=self._busy, + message=loc_text("search.busy", catalog=self._locale_catalog, default="Cerco..."), + ) def open_search_window(parent, db_app, session=None): @@ -427,4 +513,9 @@ def open_search_window(parent, db_app, session=None): w = SearchWindow(parent, db_app, session=session) setattr(parent, key, w) place_window_fullsize_below_parent_later(parent, w) + try: + w.lift() + w.focus_force() + except Exception: + pass return w diff --git a/spec_barcode_wms.rtf b/spec_barcode_wms.rtf new file mode 100644 index 0000000..d0b6fdf --- /dev/null +++ b/spec_barcode_wms.rtf @@ -0,0 +1,383 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi31507\deflang1040\deflangfe1040\themelang1040\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;} +{\f42\fbidi \fswiss\fcharset0\fprq2 Aptos;}{\f45\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0502040204020203}Segoe UI;}{\f46\fbidi \fmodern\fcharset0\fprq1{\*\panose 020b0609020204030204}Consolas;} +{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2 Aptos Display;} +{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2 Aptos;}{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\f47\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f48\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f50\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f51\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\f52\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f53\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f54\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\f55\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f387\fbidi \froman\fcharset238\fprq2 Cambria Math CE;}{\f388\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;}{\f390\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;} +{\f391\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f394\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;}{\f395\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);}{\f467\fbidi \fswiss\fcharset238\fprq2 Aptos CE;} +{\f468\fbidi \fswiss\fcharset204\fprq2 Aptos Cyr;}{\f470\fbidi \fswiss\fcharset161\fprq2 Aptos Greek;}{\f471\fbidi \fswiss\fcharset162\fprq2 Aptos Tur;}{\f474\fbidi \fswiss\fcharset186\fprq2 Aptos Baltic;} +{\f475\fbidi \fswiss\fcharset163\fprq2 Aptos (Vietnamese);}{\f497\fbidi \fswiss\fcharset238\fprq2 Segoe UI CE;}{\f498\fbidi \fswiss\fcharset204\fprq2 Segoe UI Cyr;}{\f500\fbidi \fswiss\fcharset161\fprq2 Segoe UI Greek;} +{\f501\fbidi \fswiss\fcharset162\fprq2 Segoe UI Tur;}{\f502\fbidi \fswiss\fcharset177\fprq2 Segoe UI (Hebrew);}{\f503\fbidi \fswiss\fcharset178\fprq2 Segoe UI (Arabic);}{\f504\fbidi \fswiss\fcharset186\fprq2 Segoe UI Baltic;} +{\f505\fbidi \fswiss\fcharset163\fprq2 Segoe UI (Vietnamese);}{\f507\fbidi \fmodern\fcharset238\fprq1 Consolas CE;}{\f508\fbidi \fmodern\fcharset204\fprq1 Consolas Cyr;}{\f510\fbidi \fmodern\fcharset161\fprq1 Consolas Greek;} +{\f511\fbidi \fmodern\fcharset162\fprq1 Consolas Tur;}{\f514\fbidi \fmodern\fcharset186\fprq1 Consolas Baltic;}{\f515\fbidi \fmodern\fcharset163\fprq1 Consolas (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Aptos Display CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Aptos Display Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Aptos Display Greek;} +{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Aptos Display Tur;}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Aptos Display Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Aptos Display (Vietnamese);} +{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Aptos CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Aptos Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Aptos Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Aptos Tur;} +{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Aptos Baltic;}{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Aptos (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0; +\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0; +\red31\green78\blue121;}{\*\defchp \fs24\kerning2\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\sa160\sl278\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{ +\ql \li0\ri0\sa160\sl278\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1040\langfe1040\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1040\langfenp1040 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl278\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \fs24\lang1040\langfe1040\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1040\langfenp1040 +\snext11 \ssemihidden \sunhideused Normal Table;}}{\*\rsidtbl \rsid2231436\rsid8003465\rsid8465363}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info +{\operator Alessandro Bonvicini}{\creatim\yr2026\mo5\dy12\hr19\min5}{\revtim\yr2026\mo5\dy12\hr19\min18}{\version2}{\edmins13}{\nofpages4}{\nofwords969}{\nofchars5529}{\nofcharsws6486}{\vern125}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/wo +rd/2003/wordml}}\paperw11906\paperh16838\margl1134\margr1134\margt1134\margb1134\gutter0\ltrsect +\widowctrl\ftnbj\aenddoc\hyphhotz283\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120 +\dghorigin1701\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot8003465 \fet0{\*\wgrffmtfilter 2450}\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}} +{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}} +{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9 +\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1040\langfe1040\kerning2\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1040\langfenp1040 {\rtlch\fcs1 \ab\af45\afs32 \ltrch\fcs0 \b\f45\fs32\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Specifica Form Barcode WMS +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +Documento di lavoro per replica Python del client barcode C# con miglioramenti di usabilita'. +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 1. Obiettivo operativo}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 +\f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +La form barcode guida il magazziniere nello scarico dei pallet secondo due code operative: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +- F1 = coda ad alta priorita', cioe' picking list prenotata (IDStato = 1) +\par \hich\af45\dbch\af31505\loch\f45 - F2 = coda a bassa priorita', cioe' picking list non prenotata (IDStato = 0) +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +Il magazziniere puo' passare da una coda all'altra e il sistema riprende dal punto corretto interrogando il database a ogni richiesta.}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid8003465 \hich\af45\dbch\af31505\loch\f45 + Occorre che la seconda coda che compare , \hich\af45\dbch\af31505\loch\f45 e che non pu\loch\af45\dbch\af31505\hich\f45 \'f2\loch\f45 esser\hich\af45\dbch\af31505\loch\f45 e pr\hich\af45\dbch\af31505\loch\f45 en\hich\af45\dbch\af31505\loch\f45 otata +\hich\af45\dbch\af31505\loch\f45 sul\hich\af45\dbch\af31505\loch\f45 l\hich\af45\dbch\af31505\loch\f45 \hich\f45 a desktop app se ne \'e8\hich\af45\dbch\af31505\loch\f45 \hich\af45\dbch\af31505\loch\f45 \hich\f45 gi\'e0\loch\f45 stata prenotata una +\hich\af45\dbch\af31505\loch\f45 , \hich\af45\dbch\af31505\loch\f45 sia quella \hich\af45\dbch\af31505\loch\f45 \hich\f45 pi\'f9\loch\f45 vecchi\hich\af45\dbch\af31505\loch\f45 a tra quelle esistenti \hich\af45\dbch\af31505\loch\f45 nell +\loch\af45\dbch\af31505\hich\f45 \rquote \hich\af45\dbch\af31505\loch\f45 ele\hich\af45\dbch\af31505\loch\f45 nco, c\hich\af45\dbch\af31505\loch\f45 \hich\f45 io\'e8\loch\f45 \hich\f45 quella con id del documento pi\'f9\loch\f45 basso.}{\rtlch\fcs1 +\af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 2. File C# analizzati}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 +\f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - C:_decompiled.cs +\par }{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\lang1033\langfe1040\kerning0\langnp1033\insrsid2231436\charrsid8003465 \hich\af45\dbch\af31505\loch\f45 - C:_decompiled.cs +\par \hich\af45\dbch\af31505\loch\f45 - C:_house.sql +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 3. Comandi principali della form C#}{\rtlch\fcs1 \af45\afs22 +\ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +- F1 / H Priority: imposta iStatoPkPallet = 1 e carica la prossima riga da XMag_ViewPackingList con IDStato = 1 +\par \hich\af45\dbch\af31505\loch\f45 - F2 / L Priority: imposta iStatoPkPallet = 0 e carica la prossima riga da XMag_ViewPackingList con IDStato = 0 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0\pararsid8003465 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +- Enter / Salva: esegue il movimento quando i campi sono completi}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid8003465 \hich\af45\dbch\af31505\loch\f45 , \hich\af45\dbch\af31505\loch\f45 qu\hich\af45\dbch\af31505\loch\f45 +indi questa operazione effettua un carico, ma prima l\loch\af45\dbch\af31505\hich\f45 \rquote \loch\f45 o\hich\af45\dbch\af31505\loch\f45 peratore deve premer un ta\hich\af45\dbch\af31505\loch\f45 s\hich\af45\dbch\af31505\loch\f45 to che resetta di +\hich\af45\dbch\af31505\loch\f45 \hich\f45 due campi , cio\'e8\loch\f45 li azzera e\hich\af45\dbch\af31505\loch\f45 deve comparire una b\hich\af45\dbch\af31505\loch\f45 arra rossa sul\hich\af45\dbch\af31505\loch\f45 l\hich\af45\dbch\af31505\loch\f45 +a form \hich\af45\dbch\af31505\loch\f45 del barcode\hich\af45\dbch\af31505\loch\f45 \hich\af45\dbch\af31505\loch\f45 (ver\hich\af45\dbch\af31505\loch\f45 i\hich\af45\dbch\af31505\loch\f45 ficare il c#)\hich\af45\dbch\af31505\loch\f45 , da quel momento +\hich\af45\dbch\af31505\loch\f45 se l\loch\af45\dbch\af31505\hich\f45 \rquote \hich\af45\dbch\af31505\loch\f45 o\hich\af45\dbch\af31505\loch\f45 peratore legge i barcode della udc e il bacrode della locazi\hich\af45\dbch\af31505\loch\f45 one e preme +\loch\af45\dbch\af31505\hich\f45 \'93\hich\af45\dbch\af31505\loch\f45 carica\loch\af45\dbch\af31505\hich\f45 \'94\hich\af45\dbch\af31505\loch\f45 non \loch\af45\dbch\af31505\hich\f45 \'93\hich\af45\dbch\af31505\loch\f45 salva +\loch\af45\dbch\af31505\hich\f45 \'94\hich\af45\dbch\af31505\loch\f45 \hich\af45\dbch\af31505\loch\f45 avviene il ver\hich\af45\dbch\af31505\loch\f45 sam\hich\af45\dbch\af31505\loch\f45 ento.}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 +\f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 4\hich\af45\dbch\af31505\loch\f45 +. Significato reale dei campi nella form legacy}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +La UI C# e' poco intuitiva perche' i nomi dei campi non spiegano bene il flusso. +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - txtDocRif +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 etichetta visibile: Pallet +\par \hich\af45\dbch\af31505\loch\f45 significato reale: barcode del pallet letto o da confermare +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - txtBarcode +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 etichetta visibile: Cella +\par \hich\af45\dbch\af31505\loch\f45 significato reale: ubicazione operativa di destinazione o uscita; nel picking viene impostata a 9000000 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - lblTesto1 +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 mostra ubicazione sorgente o stato operazione +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - lblTesto2 +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 mostra Documento +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - lblTesto3 +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 mostra cliente o nazione +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - lblTesto4 +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 mostra il pallet atteso}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 +\f45\fs22\cf1\kerning0\insrsid8003465 \hich\af45\dbch\af31505\loch\f45 , come passo successivo nella co\hich\af45\dbch\af31505\loch\f45 da selezionata. }{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 5. Flusso F1 e F2 nel C#}{\rtlch\fcs1 \af45\afs22 +\ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Metodo chiave: }{\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 +\f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 GetDatiPallet("", "", idStato)}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 in }{\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 +\f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 FSkMovimenti.cs}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 . +\par }{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\lang1033\langfe1040\kerning0\langnp1033\insrsid2231436\charrsid8003465 \hich\af45\dbch\af31505\loch\f45 Query utilizzata: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 \f46\fs22\cf1\lang1033\langfe1040\kerning0\langnp1033\insrsid2231436\charrsid8003465 \hich\af46\dbch\af31505\loch\f46 +SELECT TOP 1 * FROM XMag_ViewPackingList WHERE Ordinamento > 0 AND IDStato = idStato ORDER BY Ordinamento +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Effetto operativo: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - legge il prossimo pallet della coda scelta +\par \hich\af45\dbch\af31505\loch\f45 - mostra ubicazione, documento, cliente e pallet atteso +\par \hich\af45\dbch\af31505\loch\f45 - se la ricerca parte senza barcode specifico, alla fine la funzione restituisce 9000000 +\par \hich\af45\dbch\af31505\loch\f45 - per questo i pulsanti F1/F2 finiscono per scrivere 9000000 nel campo Cella +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Flusso reale per l'operatore: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 1. preme F1 oppure F2 +\par \hich\af45\dbch\af31505\loch\f45 2. il sistema mostra il prossimo pallet atteso +\par \hich\af45\dbch\af31505\loch\f45 3. il campo Cella viene impostato a 9000000 +\par \hich\af45\dbch\af31505\loch\f45 4. l'operatore scansiona il pallet nel campo Pallet +\par \hich\af45\dbch\af31505\loch\f45 5. se il pallet coincide con quello atteso, viene eseguito lo scarico +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 6. Validazione dello scan}{\rtlch\fcs1 \af45\afs22 +\ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Metodo chiave: }{\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 +\f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 SalvaOk()}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 . +\par \hich\af45\dbch\af31505\loch\f45 Regola importante: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +- se txtBarcode = 9000000, il pallet letto in txtDocRif deve coincidere con lblTesto4 +\par \hich\af45\dbch\af31505\loch\f45 - se non coincide, il sistema scrive Errata Lettura e blocca l'operazione +\par \hich\af45\dbch\af31505\loch\f45 - se coincide, chiama Ricevi(...) +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +Questa e' la protezione che impedisce di scaricare il pallet sbagliato durante il picking. +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 7. Esecuzione del movimento}{\rtlch\fcs1 \af45\afs22 +\ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Metodi chiave nel C#: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - Ricevi(...) +\par \hich\af45\dbch\af31505\loch\f45 - spt_SaveStoredProced\hich\af45\dbch\af31505\loch\f45 ure(...) +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Stored procedure usata: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 \f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 sp_xMagGestioneMagazziniPallet +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Parametri principali: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - IDOperatore +\par \hich\af45\dbch\af31505\loch\f45 - BarcodeCella +\par \hich\af45\dbch\af31505\loch\f45 - BarcodePallet +\par \hich\af45\dbch\af31505\loch\f45 - NumeroCella +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Dopo il movimento il C# richiama ancora }{\rtlch\fcs1 \af46\afs22 +\ltrch\fcs0 \f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 GetDatiPallet(sBarcodePallet, sBarcodeCella, iStatoPkPallet)}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 + per riallinearsi alla coda corrente. +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 8. Legame con la prenotazione della picking list}{ +\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 La prenotazione dal backoffice chiama la stored: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 \f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 sp_xExePackingListPallet +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Effetto DB: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - mette Celle.IDStato = 1 sulle celle del documento prenotato +\par \hich\af45\dbch\af31505\loch\f45 - se richiamata di nuovo, toglie la prenotazione riportando IDStato = 0 +\par \hich\af45\dbch\af31505\loch\f45 - registra il documento in LogPackingList +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Dopo ogni scarico, }{\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 +\f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 sp_xMagGestioneMagazziniPallet}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 richiama: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 \f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 sp_ControllaPrenotazionePackingListPalletNew +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Questa procedura: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - legge l'ultima picking list attiva dal log +\par \hich\af45\dbch\af31505\loch\f45 - riapplica IDStato = 1 alle celle residue del documento tramite }{\rtlch\fcs1 \af46\afs22 \ltrch\fcs0 \f46\fs22\cf1\kerning0\insrsid2231436 \hich\af46\dbch\af31505\loch\f46 sp_xExePackingListPalletPrenota}{\rtlch\fcs1 +\af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Quindi il comportamento risultante e': +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - F1 continua a scorrere la pick\hich\af45\dbch\af31505\loch\f45 +ing list prenotata residua +\par \hich\af45\dbch\af31505\loch\f45 - F2 continua a scorrere la coda non prenotata +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 9. Allineamento con il backoffice}{\rtlch\fcs1 \af45\afs22 +\ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Nel backoffice C# la griglia picking list: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - carica la testata aggregata da XMag_ViewPackingList +\par \hich\af45\dbch\af31505\loch\f45 - il dettaglio basso e' agganciato al solo Documento +\par \hich\af45\dbch\af31505\loch\f45 - Prenota/Sprenota richiama la stessa stored e poi ricarica davvero la griglia con InitGrid() +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Questo modello e' coerente con il comportamento della form barcode. + +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 10. Criticita' di usabilita' della form legacy}{\rtlch\fcs1 +\af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - etichette fuorvianti: Cella e Pallet non sp +\hich\af45\dbch\af31505\loch\f45 iegano cosa va letto in quel momento +\par \hich\af45\dbch\af31505\loch\f45 - il valore 9000000 compare senza contesto esplicito +\par \hich\af45\dbch\af31505\loch\f45 - non e' chiaro visivamente se si sta lavorando in F1 o in F2 +\par \hich\af45\dbch\af31505\loch\f45 - i label non parlano il linguaggio dell'operatore +\par \hich\af45\dbch\af31505\loch\f45 - la regola di validazione contro il pallet atteso non e' evidente a schermo +\par \hich\af45\dbch\af31505\loch\f45 - la UI richiede addestramento e memoria del flusso, non si lascia capire da sola +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 11. Specifica proposta per il nuovo client Python barcode}{ +\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Da mantenere identico al C#: +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - una sola picking list prenotata per volta +\par \hich\af45\dbch\af31505\loch\f45 - F1 legge IDStato = 1 +\par \hich\af45\dbch\af31505\loch\f45 - F2 legge IDStato = 0 +\par \hich\af45\dbch\af31505\loch\f45 - ordine di proposta basato su Ordinamento +\par \hich\af45\dbch\af31505\loch\f45 - scarico tramite la stessa semantica DB del C# +\par \hich\af45\dbch\af31505\loch\f45 - verifica del barcode atteso prima dello scarico verso 9000000 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 Da migliorare nella n\hich\af45\dbch\af31505\loch\f45 uova UI: + +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +- mostrare chiaramente la coda attiva: Alta priorita' (F1) o Bassa priorita' (F2) +\par \hich\af45\dbch\af31505\loch\f45 - rinominare i campi in modo esplicito +\par }\pard \ltrpar\ql \li720\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin720\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - Pallet atteso +\par \hich\af45\dbch\af31505\loch\f45 - Pallet letto +\par \hich\af45\dbch\af31505\loch\f45 - Ubicazione sorgente +\par \hich\af45\dbch\af31505\loch\f45 - Destinazione operativa +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - mostrare un messaggio guida esplicito: Scansiona il pallet indicato + +\par \hich\af45\dbch\af31505\loch\f45 - evidenziare in grande ubicazione da raggiungere e pallet atteso +\par \hich\af45\dbch\af31505\loch\f45 - separare visivamente missione, area scan ed esito operazione +\par \hich\af45\dbch\af31505\loch\f45 - mantenere il focus sempre sul campo scan +\par \hich\af45\dbch\af31505\loch\f45 - usare hotkey semplici: F1, F2, Enter +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 12. Architettura consigliata del client Python}{\rtlch\fcs1 +\af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li360\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin360\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - UI minimale in tkinter puro +\par \hich\af45\dbch\af31505\loch\f45 - service layer con logica operativa F1/F2 +\par }{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\lang1033\langfe1040\kerning0\langnp1033\insrsid2231436\charrsid8003465 \hich\af45\dbch\af31505\loch\f45 - repository layer per SQL e stored procedure +\par }{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 - timeout configurabili +\par \hich\af45\dbch\af31505\loch\f45 - reconnect DB automatico +\par \hich\af45\dbch\af31505\loch\f45 - log rotante +\par \hich\af45\dbch\af31505\loch\f45 - zero finestre multiple +\par \hich\af45\dbch\af31505\loch\f45 - stato minimo in RAM +\par }\pard \ltrpar\ql \li0\ri0\sa180\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af45\afs22 \ltrch\fcs0 \b\f45\fs22\cf19\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 13. Conclusione}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 +\f45\fs22\cf1\kerning0\insrsid2231436 +\par }\pard \ltrpar\ql \li0\ri0\sa120\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 {\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 \hich\af45\dbch\af31505\loch\f45 +Il comportamento C# della form barcode e' oggi abbastanza chiaro: e' piu' coerente di quanto sembri, ma e' esposto con una UI poco autoesplicativa. Il nuovo client Python dovrebbe quindi essere una replica funzionale fedele, ma con una UI molto piu' guida +\hich\af45\dbch\af31505\loch\f45 ta e leggibile.}{\rtlch\fcs1 \af45\afs22 \ltrch\fcs0 \f45\fs22\cf1\kerning0\insrsid2231436 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b126ca5f050800000f220000160000007468656d652f7468656d652f +7468656d65312e786d6cec5a4f6f1bb915bf17e87720e6ae78662cc9921165a1bff1267662444a8a3dd212a561cc190a43cab6b058a0c89e7a5960816dd14317 +e8ad87a2e8025da08b5efa61022468b71fa28fe468444a54fc07411114b62f33d4ef3dfef8dee37b6f38f3f0b3ab94a10b920bcab356103d080344b2319fd06c +d60a5e8e0695468084c4d904339e9156b02422f8ecd12f7ff1101fca84a404817c260e712b48a49c1feeed89310c63f180cf4906bf4d799e6209b7f96c6f92e3 +4bd09bb2bd380ceb7b29a65980329c82da11c8a00945cfa7533a26c1a395fa3e83393229d4c098e543a59c14321676721e2984588a2ecbd10566ad00669af0cb +11b99201625848f8a11584fa2fd87bf4700f1f16424cee90b5e406faaf902b0426e7b19e339f9d959386fdb8518d4afd1ac0e436aedf50ffa53e0dc0e331acd4 +70b17546b57ad8880bac0532971eddcd8368dfc55bfaf7b73847cd7a27ae3afa35c8e8af6ee1c341b3dfab39780d32f8da16be1dc69de6be83d72083af6fe1ab +fdf641dc77f01a94309a9d6fa3eb078d46bd4097902967475e78b35e0f0f7a057c8d826828a34b4d31e599dc156b297ecdf30100149061493324977332c56388 +e3f65c72817a54cc195e06688e332e60388ca30842af1ac6e5bfb6383e24d89256bc8089d81a527c9018e7742e5bc113d01a5890773ffdf4f6cd8f6fdffcfded +d75fbf7df357744c678934aa1cb9239ccd6cb99ffff4ed7fbeff35faf7dffef8f377bff5e3858d7fff97dfbcffc73f3fa41eb6dada14ef7ef7c3fb1f7f78f7fb +6ffef5e7ef3cdadb393eb3e1239a12819e914bf482a7b0406d0a973f39cb6f27314a30b525dad94ce00cab593cfafb3271d0cf9698610fae435c3bbeca21d5f8 +808f17af1dc2c3245f48ead1f834491de009e7acc373af159eaab92c338f16d9cc3f79beb0712f30bef0cdddc599e3e5fe620e3996fa547613e2d03c65389378 +46322291fa8d9f13e259dd17943a763da1e39c0b3e95e80b8a3a987a4d32a2674e34ad858e680a7e59fa0882bf1ddb9cbc421dce7cabee910b17097b03330ff9 +11618e191fe385c4a94fe508a7cc36f83196898fe470998f6d5c5f48f0f48c308efa1322844fe6790eebb59cfe144376f3bafd842d5317994b7aeed3798c39b7 +913d7ede4d703af76187344b6cece7e21c4214a3532e7df013eeee10750f7ec0d94e77bfa2c471f7f5d9e02564399bd23a40d42f8bdce3cbc7843bf13b5cb229 +26be54d3ce5327c5b673ea8d8ece62e684f631210c5fe20921e8e5e71e061d3e776cbe26fd2481ac72447c81f504bbb1aaee332208d2cdcd769e3ca6c209d921 +99f11d7c4e961b896789b314e7bb343f03afdb36ef9fe5b0193deb7ccec6e736f019852e10e2c56b94e7027458c1bd53eb69829d02a6ee853f5e97b9e3bf9bec +31d897af1d1a37d89720436e2d0389dd96f9a06d46983913ac036684293af6a55b1071dcbf1651c5558b2dbc725377d3aedd00dd91d3f4a434bba603fadf753e +d05fbcfbc3f79e10fc38dd8e5fb193aa6ed9e7ec4a25471bddcd2edc664fd3e5f9847efa2d4d0f2fb2530255643b5fdd7734f71d4df07fdfd1ecdacff77dccae +6ee3be8f09a0bfb8ef638aa3958fd3c7ac5b17e86ad4f18239e6d1873ee9ce339f29656c28978c1c0b7dec23e0696632804125a74f3c497906384fe052953998 +c0c1cd72ac6550cee5afa84c86099ec3d9501428253351a89e0934e7028e8cf4b057b7c2b3457ac227e6a8539f2d85a6b20a2cd7e3610d0e9dcc381c534983ae +1f14838a9f3e4f05be9aed4c1fb3ae0828d9db90b0267349ec7b481cac06af21a14ecd3e0e8ba6874543a95fb96acb1440adf40a3c6e2378486f05b5aa2204a7 +e4620cadf944f9c9b87ae55dedcc8fe9e95dc67422008e15cd4ae058bef4745371ddb93cb53a136a37f0b443423bc584954b425b463778228187e0223ad5e84d +68dcd6d7cdb54b1d7aca147a3e88ef358d83c68758dcd5d720b7991b5866670a96a14bd8e3316cba008df1bc154ce1cc182ed339048f508f5c98cde0d5cb58e6 +66c7df25b5cc73217b5824c6e23aeb18ffa454921c319ab602b5fed20f2cd349c4906bc2d6fd54c9c56ac37d6ae4c0ebae97c9744ac6d2f6bb35a22c6d6e21c5 +9b64e1fd558bdf1dac24f902dc3d4c2697e88c2df2171842ac761029ef4ea88057079171f584c2bbb03293ade36fa33215d9df7e19a563c88c63364f705152ec +6c6ee0baa09474f45d6903ebae583318d432495109cf66aac2da4675ca6959bb0c879d65f77a2165392b6bae8ba6935654d9f4a7316786551dd8b0e5ddaabcc5 +6a6562486a768937b97b33e73657c96ea35128cb0418bcb4dfdd6abf456d3d99434d31decec32a6917a36ef1582df01a6a37a91256daafafd46ed8ad2c12dee9 +60f04ea51fe436a31686a6abc6525b5abf36b7df6bf3b3d7903c7ad0e62e9879d3cd32b8535129e6a7b9f6ed199f2c8b4b264ca2313e574da942b2ec0599223a +b96a05b1af73346f5ba3a21bd06825a68a5729e8edf65cc102af44cd862d854d8097416536a52b5c4ae899a1f72e85f589a28fb6bc5a5156bd3ae0b509855935 +98b6b0145c6d5b115efde7187adba1eeec4cee05da57b2c82f708516396d055f86b576b51bd7ba95b051eb57aafbd5b0d2a8b5f72bed5a6d3fead7a2b0d789bf +027a3249a39af9ee61002f81d8b2f8fa418f6f7d0191aede733d18f3748feb2f1bf6b4f7f5171051ec7c0161be664023f58143008e045a713faac6edb85be9f6 +a27aa51af7ea95c6c17ebbd28debbdb80d45bb3e687f15a00b0d8e3abdde60508b2bf52ee0aa61bb566977f6bb957aa3df890751bfda0b015c949f2b788a019b +ad6c01979ad7a3ff020000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f74 +68656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be99 +69bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae +38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bba +bac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e74 +5f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f2e +72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f7468 +656d654d616e616765722e786d6c504b01022d0014000600080000002100b126ca5f050800000f2200001600000000000000000000000000d60200007468656d +652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b01000027000000000000000000000000000f0b00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d0100000a0c00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; +\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text; +\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2; +\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List; +\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1; +\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision; +\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1; +\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1; +\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; +\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2; +\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; +\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3; +\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4; +\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; +\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4; +\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5; +\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000 +02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000404d +a74a33e2dc01feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/spec_barcode_wms_aggiornata.rtf b/spec_barcode_wms_aggiornata.rtf new file mode 100644 index 0000000..c7cd8c4 --- /dev/null +++ b/spec_barcode_wms_aggiornata.rtf @@ -0,0 +1,170 @@ +{\rtf1\ansi\deff0 +{\fonttbl +{\f0 Segoe UI;} +{\f1 Consolas;} +} +\fs24 +\pard\b\f0\fs32 Specifica Form Barcode WMS\par +\pard\b0\fs22 Documento di lavoro aggiornato per replica Python del client barcode C# con miglioramenti di usabilita'.\par +\par +\pard\b 1. Obiettivo operativo\par +\pard\b0 La form barcode guida il magazziniere in movimenti WMS di due tipi:\par +\pard\li360 - prelievo verso la cella virtuale 9000000\par +\pard\li360 - versamento verso una cella reale di magazzino\par +\pard\li0 Entrambe le operazioni usano lo stesso motore di movimento DB; cambia la destinazione operativa.\par +\par +\pard\b 2. Code F1 / F2\par +\pard\b0 Il comportamento reale ricostruito e confermato e' il seguente:\par +\pard\li360 - F1 = coda ad alta priorita', cioe' picking list prenotata (IDStato = 1)\par +\pard\li360 - F2 = coda a bassa priorita', cioe' coda non prenotata (IDStato = 0)\par +\pard\li0 Dall'interfaccia backoffice si puo' prenotare una sola picking list per volta. La lista prenotata entra in F1; la seconda coda operativa viene proposta con F2.\par +\par +\pard\b 3. File C# analizzati\par +\pard\b0\li360 - FSkMovimenti.cs\par +\pard\li360 - FSkAccettazione.cs\par +\pard\li360 - FRMagViewLayout.cs\par +\pard\li360 - GridViewColumnButtonMenu.cs\par +\pard\li360 - script.sql\par +\par +\pard\b 4. Comandi principali della form C#\par +\pard\b0\li360 - F1 / H Priority: imposta iStatoPkPallet = 1 e carica la prossima riga da XMag_ViewPackingList con IDStato = 1\par +\pard\li360 - F2 / L Priority: imposta iStatoPkPallet = 0 e carica la prossima riga da XMag_ViewPackingList con IDStato = 0\par +\pard\li360 - Enter / Salva: esegue il movimento quando i campi sono completi\par +\pard\li360 - F4 / Elimina: forza uno scarico verso 9000000\par +\par +\pard\b 5. Significato reale dei campi nella form legacy\par +\pard\b0 La UI C# e' poco intuitiva perche' i nomi dei campi non spiegano il flusso.\par +\pard\li360 - txtDocRif\par +\pard\li720 etichetta visibile: Pallet\par +\pard\li720 significato reale: barcode del pallet letto o da confermare\par +\pard\li360 - txtBarcode\par +\pard\li720 etichetta visibile: Cella\par +\pard\li720 significato reale: destinazione operativa; nel picking viene impostata a 9000000\par +\pard\li360 - lblTesto1\par +\pard\li720 e' il vero indicatore di stato; viene usato anche come barra colorata\par +\pard\li360 - lblTesto2\par +\pard\li720 Documento nella fase di proposta picking; riga informativa variabile nella fase di conferma\par +\pard\li360 - lblTesto3\par +\pard\li720 cliente / nazione nella fase di proposta picking; riga informativa variabile nella fase di conferma\par +\pard\li360 - lblTesto4\par +\pard\li720 pallet atteso nella fase di proposta picking; riga informativa variabile nella fase di conferma\par +\par +\pard\b 6. Query principale F1 / F2\par +\pard\b0 Metodo chiave nel C#: \f1 GetDatiPallet("", "", idStato)\f0\par +\pard\li360 Query:\par +\pard\li720\f1 SELECT TOP 1 * FROM XMag_ViewPackingList WHERE Ordinamento > 0 AND IDStato = {idStato} ORDER BY Ordinamento\f0\par +\pard\li0 Effetto reale:\par +\pard\li360 1. viene scelta la prossima UDC della coda selezionata\par +\pard\li360 2. vengono riempiti i label con ubicazione, documento, cliente e pallet atteso\par +\pard\li360 3. il campo Cella viene impostato a 9000000\par +\pard\li360 4. l'operatore legge il pallet nel campo Pallet\par +\pard\li360 5. se il pallet coincide con quello atteso, viene eseguito il prelievo\par +\par +\pard\b 7. Reset dei campi e barra rossa / verde\par +\pard\b0 Ricostruzione aggiornata dal C#:\par +\pard\li360 - non emerge un pulsante esplicito che entri formalmente in una "modalita' versamento"\par +\pard\li360 - i reset vengono fatti in modo automatico dentro \f1 GetDatiPallet(...)\f0\ e \f1 GetDatiPalletLotto(...)\f0\par +\pard\li360 - all'inizio di questi metodi il C# pulisce i label, azzera \f1 txtDocRif\f0\ e imposta \f1 lblTesto1.BackColor = Red\f0\par +\pard\li360 - quando il dato viene trovato correttamente, \f1 lblTesto1\f0\ passa a \f1 LightGreen\f0\ o \f1 GreenYellow\f0\par +\pard\li0 Quindi la "barra rossa / verde" ricordata dall'operatore coincide con \f1 lblTesto1\f0, non con un controllo separato.\par +\par +\pard\b 8. Validazione dello scan\par +\pard\b0 Metodo chiave: \f1 SalvaOk()\f0\par +\pard\li360 - se \f1 txtBarcode = 9000000\f0, il pallet letto in \f1 txtDocRif\f0\ deve coincidere con \f1 lblTesto4\f0\par +\pard\li360 - se non coincide: errore \f1 Errata Lettura\f0\ e blocco operazione\par +\pard\li360 - se coincide: chiamata a \f1 Ricevi(...)\f0\par +\pard\li0 Questa e' la protezione che impedisce di scaricare il pallet sbagliato durante il picking.\par +\par +\pard\b 9. Esecuzione del movimento\par +\pard\b0 Metodi chiave nel C#:\par +\pard\li360 - \f1 Ricevi(...)\f0\par +\pard\li360 - \f1 spt_SaveStoredProcedure(...)\f0\par +\pard\li0 Stored procedure usata:\par +\pard\li360\f1 sp_xMagGestioneMagazziniPallet\f0\par +\pard\li0 Parametri principali:\par +\pard\li360 - IDOperatore\par +\pard\li360 - BarcodeCella\par +\pard\li360 - BarcodePallet\par +\pard\li360 - NumeroCella\par +\pard\li0 Dopo il movimento il C# richiama ancora \f1 GetDatiPallet(sBarcodePallet, sBarcodeCella, iStatoPkPallet)\f0.\par +\pard\li0 I messaggi di conferma vengono quindi scritti a valle della transazione DB, non prima.\par +\par +\pard\b 10. Convenzioni logistiche emerse dalla verifica sul campo\par +\pard\b0 La verifica con il magazziniere ha chiarito il significato delle celle convenzionali usate dai fallback del sistema.\par +\pard\li360 - 9000000 = destinazione logica di input usata dalla form per il prelievo\par +\pard\li360 - 9999 = locazione convenzionale 7G.1.1\par +\pard\li360 - 1000 = locazione convenzionale 5E1.1\par +\pard\li0 Questo significa che:\par +\pard\li360 - una UDC prelevata dalla picking list finisce logicamente verso 9000000, ma viene poi rappresentata operativamente come 7G.1.1\par +\pard\li360 - una UDC non scaffalata nella picking list viene proposta con la locazione convenzionale 5E1.1\par +\pard\li0 I fallback che sembravano "sporchi" nel codice sono quindi parte della semantica reale del magazzino.\par +\par +\pard\b 11. Comportamento osservato nel prelievo picking list\par +\pard\b0 Flusso operativo confermato:\par +\pard\li360 1. il magazziniere preme F1 o F2\par +\pard\li360 2. il barcode mostra la prossima UDC della coda scelta con la sua locazione\par +\pard\li360 3. l'operatore legge il pallet atteso\par +\pard\li360 4. il movimento parte verso la destinazione logica 9000000\par +\pard\li360 5. alla conferma compare \f1 Ok Scarico\f0\ nella prima label\par +\pard\li360 6. compare \f1 7G.1.1\f0\ come locazione convenzionale di spedito\par +\pard\li360 7. dopo il tempo della logica lato server viene proposta la UDC successiva\par +\pard\li0 Questo comportamento percepito dall'operatore e' compatibile con il fatto che le label vengano aggiornate dopo il ritorno della stored.\par +\par +\pard\b 12. Caso UDC non scaffalata\par +\pard\b0 Nella vista SQL la UDC non scaffalata usa un fallback tecnico, ma per l'operatore la locazione visibile e significativa e':\par +\pard\li360 - 5E1.1\par +\pard\li0 Quindi il client Python non deve mostrare soltanto la stringa tecnica `Non scaff.`, ma deve privilegiare la convenzione operativa 5E1.1 dove il legacy la rende significativa.\par +\par +\pard\b 13. Legame con la prenotazione della picking list\par +\pard\b0 La prenotazione backoffice chiama:\par +\pard\li360\f1 sp_xExePackingListPallet\f0\par +\pard\li0 Effetto DB:\par +\pard\li360 - mette \f1 Celle.IDStato = 1\f0\ sulle celle del documento prenotato\par +\pard\li360 - se richiamata di nuovo, riporta \f1 IDStato = 0\f0\par +\pard\li360 - scrive il documento in \f1 LogPackingList\f0\par +\pard\li0 Dopo ogni prelievo, \f1 sp_xMagGestioneMagazziniPallet\f0\ richiama:\par +\pard\li360\f1 sp_ControllaPrenotazionePackingListPalletNew\f0\par +\pard\li0 Questa procedura ri-prenota automaticamente le celle residue della picking list attiva.\par +\pard\li0 Quindi:\par +\pard\li360 - F1 continua a scorrere la picking list prenotata residua\par +\pard\li360 - F2 continua a scorrere la coda non prenotata\par +\par +\pard\b 14. Tempi percepiti dall'operatore\par +\pard\b0 Nel C# non emerge un vero timer di reset a 2 secondi. Il ritardo percepito dal magazziniere e' piu' coerente con:\par +\pard\li360 - tempo di transazione DB\par +\pard\li360 - tempo di riesecuzione della logica server\par +\pard\li360 - tempo di riaggancio alla UDC successiva\par +\pard\li0 Quindi il "reset" percepito e' in realta' la finestra temporale durante cui il terminale attende l'esito e poi aggiorna le label.\par +\par +\pard\b 15. Allineamento richiesto nel client Python\par +\pard\b0 Da mantenere uguale al C# e alla prassi operativa:\par +\pard\li360 - una sola picking list prenotata per volta\par +\pard\li360 - F1 legge IDStato = 1\par +\pard\li360 - F2 legge IDStato = 0\par +\pard\li360 - proposta UDC ordinata da DB\par +\pard\li360 - movimento tramite la stessa stored legacy \f1 sp_xMagGestioneMagazziniPallet\f0\par +\pard\li360 - validazione del pallet atteso prima del prelievo verso 9000000\par +\pard\li360 - visualizzazione di 7G.1.1 come locazione convenzionale di spedito\par +\pard\li360 - visualizzazione di 5E1.1 per le UDC non scaffalate\par +\par +\pard\b 16. Criticita' di usabilita' della form legacy\par +\pard\b0\li360 - etichette fuorvianti: Cella e Pallet non spiegano cosa va letto in quel momento\par +\pard\li360 - il valore 9000000 compare senza contesto esplicito\par +\pard\li360 - non e' chiaro visivamente se si sta lavorando in F1 o in F2\par +\pard\li360 - la validazione contro il pallet atteso non e' evidente a schermo\par +\pard\li360 - il flusso dipende da reset e colori impliciti, quindi richiede addestramento\par +\par +\pard\b 17. Specifica proposta per il nuovo client Python barcode\par +\pard\b0 Replica funzionale fedele, ma con UI piu' guidata.\par +\pard\li360 - una sola finestra tkinter pura\par +\pard\li360 - service layer separato dalla UI\par +\pard\li360 - repository layer separato dall'accesso SQL\par +\pard\li360 - focus sempre sul campo scansione corretto\par +\pard\li360 - stato visivo chiaro: F1, F2, versamento, conferma\par +\pard\li360 - messaggi espliciti per l'operatore: cosa leggere adesso\par +\pard\li360 - barra stato grande con colori coerenti al legacy\par +\pard\li360 - gestione esplicita delle locazioni convenzionali 5E1.1 e 7G.1.1\par +\par +\pard\b 18. Conclusione\par +\pard\b0 Il comportamento C# della form barcode e' abbastanza chiaro: e' piu' coerente di quanto sembri, ma e' esposto con una UI poco autoesplicativa. Il client Python deve quindi mantenere la semantica legacy lato DB e coda operativa, ma renderla finalmente leggibile e stabile per l'operatore.\par +} diff --git a/tooltip.json b/tooltip.json index 08f29ef..f917ea5 100644 --- a/tooltip.json +++ b/tooltip.json @@ -8,6 +8,18 @@ "launcher.open_pickinglist": "Apre la gestione delle picking list per prenotare, controllare e aggiornare le liste di prelievo.", "launcher.arrange_windows": "Dispone in cascata le finestre aperte seguendo l'ordine dei pulsanti del launcher.", "launcher.exit": "Chiude l'applicazione in modo pulito terminando la sessione utente e rilasciando la connessione condivisa al database.", + "dbconfig.heading": "Spiega che qui si inseriscono i dati minimi per permettere al programma di collegarsi al database del magazzino al primo avvio.", + "dbconfig.field.server": "Nome server o indirizzo IP dell'istanza SQL Server che ospita il database del magazzino.", + "dbconfig.field.database": "Nome del database applicativo da usare per il WMS.", + "dbconfig.field.user": "Utente SQL Server da usare per l'accesso al database.", + "dbconfig.field.password": "Password dell'utente SQL Server configurato per il collegamento.", + "dbconfig.field.driver": "Driver ODBC installato sul PC che verra' usato da Python per collegarsi a SQL Server.", + "dbconfig.field.encrypt": "Valore opzionale del parametro Encrypt. Lascialo vuoto se non serve una configurazione specifica.", + "dbconfig.field.trust_server_certificate": "Attiva la fiducia sul certificato del server SQL quando l'ambiente usa certificati locali o autofirmati.", + "dbconfig.info": "Il file di configurazione viene salvato una sola volta sul PC e riutilizzato ai successivi avvii del programma.", + "dbconfig.button.cancel": "Chiude la configurazione iniziale senza salvare. In questo caso il programma non prosegue.", + "dbconfig.button.test": "Prova subito la connessione con i dati inseriti senza ancora salvarli definitivamente.", + "dbconfig.button.save": "Verifica i dati, salva il file di configurazione e permette al programma di continuare l'avvio.", "reset_corsie.refresh": "Ricarica il riepilogo e l'elenco delle celle occupate per la corsia selezionata.", "reset_corsie.empty_aisle": "Scarica logicamente tutte le UDC attive della corsia selezionata verso l'ubicazione di uscita.", "layout.search_udc": "Cerca una UDC per barcode, cambia automaticamente corsia e porta in evidenza la cella trovata.", @@ -28,6 +40,18 @@ "launcher.open_pickinglist": "Open picking list management to reserve, inspect and update picking lists.", "launcher.arrange_windows": "Arrange open windows in cascade order following the launcher buttons.", "launcher.exit": "Close the application cleanly by ending the user session and releasing the shared database connection.", + "dbconfig.heading": "Explain that this form collects the minimum data needed to connect the program to the warehouse database on first startup.", + "dbconfig.field.server": "SQL Server instance name or IP address hosting the warehouse database.", + "dbconfig.field.database": "Application database name used by the WMS.", + "dbconfig.field.user": "SQL Server user used to access the database.", + "dbconfig.field.password": "Password for the configured SQL Server user.", + "dbconfig.field.driver": "ODBC driver installed on the PC that Python will use to connect to SQL Server.", + "dbconfig.field.encrypt": "Optional Encrypt parameter value. Leave it blank if no specific encryption setting is required.", + "dbconfig.field.trust_server_certificate": "Enable trust for the SQL Server certificate when the environment uses local or self-signed certificates.", + "dbconfig.info": "The configuration file is saved once on the PC and reused on the next application startups.", + "dbconfig.button.cancel": "Close the first-run configuration without saving. In this case the program will not continue.", + "dbconfig.button.test": "Try the connection immediately with the entered values without saving them yet.", + "dbconfig.button.save": "Validate the connection, save the configuration file and let the program continue startup.", "reset_corsie.refresh": "Reload the summary and the list of occupied cells for the selected aisle.", "reset_corsie.empty_aisle": "Logically unload all active UDCs in the selected aisle to the outbound location.", "layout.search_udc": "Search a UDC by barcode, switch aisle automatically and highlight the matching cell.", diff --git a/ui_theme.json b/ui_theme.json index a05f7c3..77be64a 100644 --- a/ui_theme.json +++ b/ui_theme.json @@ -161,5 +161,111 @@ "size": 12, "weight": "bold" } + }, + "login_window": { + "window_geometry": "420x250", + "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], + "overlay_panel_fg_color": ["#f2f2f2", "#353535"], + "overlay_panel_corner_radius": 10, + "overlay_label_font": { + "family": "Segoe UI", + "size": 11, + "weight": "bold" + }, + "overlay_progress_width": 220, + "overlay_label_padding": [18, 14, 18, 8], + "overlay_progress_padding": [18, 0, 18, 14] + }, + "search_window": { + "window_geometry": "1100x720", + "window_minsize": [900, 560], + "window_fg_color": ["#efefef", "#2f2f2f"], + "toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"], + "toolbar_button_font": { + "family": "Segoe UI", + "size": 10, + "weight": "bold" + }, + "toolbar_label_font": { + "family": "Segoe UI", + "size": 10, + "weight": "normal" + }, + "entry_font": { + "family": "Segoe UI", + "size": 10, + "weight": "normal" + }, + "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], + "overlay_panel_fg_color": ["#f2f2f2", "#353535"], + "overlay_panel_corner_radius": 10, + "overlay_label_font": { + "family": "Segoe UI", + "size": 11, + "weight": "bold" + }, + "overlay_progress_width": 220, + "overlay_label_padding": [18, 14, 18, 8], + "overlay_progress_padding": [18, 0, 18, 14] + }, + "scarico_dialog": { + "header_font": { + "family": "Segoe UI", + "size": 13, + "weight": "bold" + }, + "body_font": { + "family": "Segoe UI", + "size": 10, + "weight": "normal" + }, + "button_font": { + "family": "Segoe UI", + "size": 10, + "weight": "bold" + }, + "tree_heading_font": { + "family": "Segoe UI", + "size": 10, + "weight": "bold" + }, + "tree_body_font": { + "family": "Segoe UI", + "size": 10, + "weight": "normal" + }, + "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], + "overlay_panel_fg_color": ["#f2f2f2", "#353535"], + "overlay_panel_corner_radius": 10, + "overlay_label_font": { + "family": "Segoe UI", + "size": 11, + "weight": "bold" + }, + "overlay_progress_width": 220, + "overlay_label_padding": [18, 14, 18, 8], + "overlay_progress_padding": [18, 0, 18, 14] + }, + "pickinglist_window": { + "window_geometry": "1200x700", + "window_minsize": [1000, 560], + "window_fg_color": ["#efefef", "#2f2f2f"], + "toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"], + "toolbar_button_font": { + "family": "Segoe UI", + "size": 10, + "weight": "bold" + }, + "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], + "overlay_panel_fg_color": ["#f2f2f2", "#353535"], + "overlay_panel_corner_radius": 10, + "overlay_label_font": { + "family": "Segoe UI", + "size": 11, + "weight": "bold" + }, + "overlay_progress_width": 220, + "overlay_label_padding": [18, 14, 18, 8], + "overlay_progress_padding": [18, 0, 18, 14] } } diff --git a/view_celle_multi_udc.py b/view_celle_multi_udc.py index 52bda4e..c79e731 100644 --- a/view_celle_multi_udc.py +++ b/view_celle_multi_udc.py @@ -19,6 +19,7 @@ from openpyxl.styles import Alignment, Font from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from gestione_scarico import move_pallet_async +from locale_text import load_locale_catalog, text as loc_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from ui_theme import theme_color, theme_font, theme_section, theme_value from window_placement import place_window_fullsize_below_parent_later @@ -433,8 +434,9 @@ class CelleMultipleWindow(ctk.CTkToplevel): """Bind the shared DB client and immediately load the tree summary.""" super().__init__(root) self._theme = theme_section("multi_udc", {}) + self._locale_catalog = load_locale_catalog() self._tooltip_catalog = load_tooltip_catalog() - self.title("Celle con piu' pallet") + self.title(loc_text("multi.title", catalog=self._locale_catalog, default="Celle con piu' pallet")) self.session = session self.geometry(str(theme_value(self._theme, "window_geometry", "1100x700"))) minsize = theme_value(self._theme, "window_minsize", [900, 550]) @@ -469,17 +471,17 @@ class CelleMultipleWindow(ctk.CTkToplevel): toolbar.configure(fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b"))) except Exception: pass - btn_refresh = ctk.CTkButton(toolbar, text="Aggiorna", command=self.refresh_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_refresh = ctk.CTkButton(toolbar, text=loc_text("multi.button.refresh", catalog=self._locale_catalog, default="Aggiorna"), command=self.refresh_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_refresh.pack(side="left", padx=6, pady=4) - btn_expand = ctk.CTkButton(toolbar, text="Espandi tutto", command=self.expand_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_expand = ctk.CTkButton(toolbar, text=loc_text("multi.button.expand", catalog=self._locale_catalog, default="Espandi tutto"), command=self.expand_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_expand.pack(side="left", padx=6, pady=4) - btn_collapse = ctk.CTkButton(toolbar, text="Comprimi tutto", command=self.collapse_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_collapse = ctk.CTkButton(toolbar, text=loc_text("multi.button.collapse", catalog=self._locale_catalog, default="Comprimi tutto"), command=self.collapse_all, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_collapse.pack(side="left", padx=6, pady=4) - btn_preselect = ctk.CTkButton(toolbar, text="Preselezione fantasmi corsia", command=self._preselect_selected_corsia, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_preselect = ctk.CTkButton(toolbar, text=loc_text("multi.button.preselect", catalog=self._locale_catalog, default="Preselezione fantasmi corsia"), command=self._preselect_selected_corsia, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_preselect.pack(side="left", padx=6, pady=4) - btn_remove = ctk.CTkButton(toolbar, text="Rimuovi fantasmi corsia", command=self._remove_selected_ghosts_for_corsia, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_remove = ctk.CTkButton(toolbar, text=loc_text("multi.button.remove", catalog=self._locale_catalog, default="Rimuovi fantasmi corsia"), command=self._remove_selected_ghosts_for_corsia, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_remove.pack(side="left", padx=6, pady=4) - btn_export = ctk.CTkButton(toolbar, text="Esporta in XLSX", command=self.export_to_xlsx, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) + btn_export = ctk.CTkButton(toolbar, text=loc_text("multi.button.export", catalog=self._locale_catalog, default="Esporta in XLSX"), command=self.export_to_xlsx, font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold"))) btn_export.pack(side="left", padx=6, pady=4) WidgetToolTip(btn_refresh, tooltip_text("multi_udc.refresh", catalog=self._tooltip_catalog)) WidgetToolTip(btn_expand, tooltip_text("multi_udc.expand_all", catalog=self._tooltip_catalog)) @@ -520,7 +522,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): pass ctk.CTkLabel( sumf, - text="Riepilogo % celle multiple per corsia", + text=loc_text("multi.summary", catalog=self._locale_catalog, default="Riepilogo % celle multiple per corsia"), font=theme_font(self._theme, "summary_title_font", ("Segoe UI", 12, "bold")), ).pack(anchor="w", padx=8, pady=(8, 0)) inner = ctk.CTkFrame(sumf) @@ -645,7 +647,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore caricamento corsie UDC fantasma: {ex}") messagebox.showerror("Errore", str(ex), parent=self) - self.runner.run(_q(self.db), self._fill_corsie, _err) + self.runner.run( + _q(self.db), + self._fill_corsie, + _err, + busy=self._busy, + message="Carico corsie UDC fantasma...", + ) @_log_call() def _fill_corsie(self, res): @@ -700,7 +708,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore caricamento celle duplicate corsia={corsia}: {ex}") messagebox.showerror("Errore", str(ex), parent=self) - self.runner.run(_q(self.db), lambda res: self._fill_celle(parent_iid, res), _err) + self.runner.run( + _q(self.db), + lambda res: self._fill_celle(parent_iid, res), + _err, + busy=self._busy, + message=f"Carico celle duplicate corsia {corsia}...", + ) @_log_call() def _fill_celle(self, parent_iid, res): @@ -744,7 +758,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore caricamento pallet cella idcella={idcella}: {ex}") messagebox.showerror("Errore", str(ex), parent=self) - self.runner.run(_q(self.db), lambda res: self._fill_pallet(parent_iid, res), _err) + self.runner.run( + _q(self.db), + lambda res: self._fill_pallet(parent_iid, res), + _err, + busy=self._busy, + message=f"Carico pallet della cella {idcella}...", + ) @_log_call() def _fill_pallet(self, parent_iid, res): @@ -859,7 +879,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore preselezione fantasmi corsia={corsia}: {ex}") messagebox.showerror("Errore", str(ex), parent=self) - self.runner.run(_q(self.db), _ok, _err) + self.runner.run( + _q(self.db), + _ok, + _err, + busy=self._busy, + message=f"Preselezione fantasmi corsia {corsia.strip()}...", + ) @_log_call() def _remove_selected_ghosts_for_corsia(self): @@ -926,7 +952,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore bonifica fantasmi corsia={corsia}: {ex}") messagebox.showerror("Errore bonifica", str(ex), parent=self) - self.runner.run(_q(self.db), _ok, _err) + self.runner.run( + _q(self.db), + _ok, + _err, + busy=self._busy, + message=f"Rimozione fantasmi corsia {corsia.strip()}...", + ) @_log_call() def _load_riepilogo(self): @@ -940,7 +972,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): _MODULE_LOGGER.exception(f"Errore caricamento riepilogo UDC fantasma: {ex}") messagebox.showerror("Errore", str(ex), parent=self) - self.runner.run(_q(self.db), self._fill_riepilogo, _err) + self.runner.run( + _q(self.db), + self._fill_riepilogo, + _err, + busy=self._busy, + message="Carico riepilogo UDC fantasma...", + ) @_log_call() def _fill_riepilogo(self, res): diff --git a/window_placement.py b/window_placement.py index d967eb4..75df5a5 100644 --- a/window_placement.py +++ b/window_placement.py @@ -76,6 +76,129 @@ def _work_area_bounds(window: tk.Misc) -> tuple[int, int, int, int]: return 0, 0, int(window.winfo_screenwidth()), int(window.winfo_screenheight()) +def _taskbar_thickness(window: tk.Misc) -> int: + """Return the Windows taskbar thickness when it can be determined.""" + + try: + screen_h = int(window.winfo_screenheight()) + _left, _top, _right, work_bottom = _work_area_bounds(window) + inferred = max(0, screen_h - int(work_bottom)) + if inferred > 0: + return inferred + except Exception: + pass + + try: + if hasattr(ctypes, "windll"): + class RECT(ctypes.Structure): + _fields_ = [ + ("left", ctypes.c_long), + ("top", ctypes.c_long), + ("right", ctypes.c_long), + ("bottom", ctypes.c_long), + ] + + class APPBARDATA(ctypes.Structure): + _fields_ = [ + ("cbSize", ctypes.c_uint), + ("hWnd", ctypes.c_void_p), + ("uCallbackMessage", ctypes.c_uint), + ("uEdge", ctypes.c_uint), + ("rc", RECT), + ("lParam", ctypes.c_long), + ] + + ABM_GETTASKBARPOS = 0x00000005 + abd = APPBARDATA() + abd.cbSize = ctypes.sizeof(APPBARDATA) + if ctypes.windll.shell32.SHAppBarMessage(ABM_GETTASKBARPOS, ctypes.byref(abd)): + rect = abd.rc + width = max(0, int(rect.right) - int(rect.left)) + height = max(0, int(rect.bottom) - int(rect.top)) + return max(width, height) + except Exception: + pass + + return 0 + + +def _window_nonclient_extra(window: tk.Misc) -> tuple[int, int]: + """Return extra outer frame size added by Windows around the client area.""" + + try: + if not hasattr(ctypes, "windll"): + return 0, 0 + + class RECT(ctypes.Structure): + _fields_ = [ + ("left", ctypes.c_long), + ("top", ctypes.c_long), + ("right", ctypes.c_long), + ("bottom", ctypes.c_long), + ] + + class POINT(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_long), + ("y", ctypes.c_long), + ] + + user32 = ctypes.windll.user32 + hwnd = int(window.winfo_id()) + outer = RECT() + client = RECT() + origin = POINT() + if not user32.GetWindowRect(hwnd, ctypes.byref(outer)): + return 0, 0 + if not user32.GetClientRect(hwnd, ctypes.byref(client)): + return 0, 0 + if not user32.ClientToScreen(hwnd, ctypes.byref(origin)): + return 0, 0 + + outer_w = max(0, int(outer.right) - int(outer.left)) + outer_h = max(0, int(outer.bottom) - int(outer.top)) + client_w = max(0, int(client.right) - int(client.left)) + client_h = max(0, int(client.bottom) - int(client.top)) + extra_w = max(0, outer_w - client_w) + extra_h = max(0, outer_h - client_h) + return extra_w, extra_h + except Exception: + return 0, 0 + + +def _window_outer_bounds(window: tk.Misc) -> tuple[int, int, int, int] | None: + """Return the actual outer window rect in screen coordinates.""" + + try: + if not hasattr(ctypes, "windll"): + return None + + class RECT(ctypes.Structure): + _fields_ = [ + ("left", ctypes.c_long), + ("top", ctypes.c_long), + ("right", ctypes.c_long), + ("bottom", ctypes.c_long), + ] + + rect = RECT() + hwnd = int(window.winfo_id()) + if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)): + return None + return int(rect.left), int(rect.top), int(rect.right), int(rect.bottom) + except Exception: + return None + + +def _set_window_alpha(window: tk.Misc, alpha: float) -> None: + """Best-effort helper to change a window opacity.""" + + try: + window.attributes("-alpha", float(alpha)) + except Exception: + pass + + def _set_window_bounds(child: tk.Misc, x: int, y: int, width: int | None = None, height: int | None = None) -> None: """Move a toplevel to the requested bounds, resizing it when dimensions are provided.""" @@ -146,6 +269,21 @@ def _set_window_position(child: tk.Misc, x: int, y: int) -> None: _set_window_bounds(child, x, y, None, None) +def _ensure_window_position(child: tk.Misc, x: int, y: int, *, tolerance: int = 2) -> None: + """Only correct the window position when it really drifted from the target.""" + + try: + current_x, current_y = _safe_xy(child) + if current_x is None or current_y is None: + _set_window_position(child, x, y) + return + if abs(current_x - int(x)) <= tolerance and abs(current_y - int(y)) <= tolerance: + return + except Exception: + pass + _set_window_position(child, x, y) + + def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[int, int]]) -> bool: """Try to reposition multiple windows in one Win32 batch to reduce flicker.""" @@ -156,6 +294,11 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in try: user32 = ctypes.windll.user32 + WM_SETREDRAW = 0x000B + RDW_INVALIDATE = 0x0001 + RDW_ALLCHILDREN = 0x0080 + RDW_FRAME = 0x0400 + redraw_hwnds: list[int] = [] hdwp = user32.BeginDeferWindowPos(len(windows)) if not hdwp: return False @@ -163,7 +306,9 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in SWP_NOSIZE = 0x0001 SWP_NOZORDER = 0x0004 SWP_NOACTIVATE = 0x0010 - flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE + SWP_NOREDRAW = 0x0008 + SWP_DEFERERASE = 0x2000 + flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW | SWP_DEFERERASE for child, (x, y) in zip(windows, positions): try: @@ -175,15 +320,43 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in except Exception: pass hwnd = int(child.winfo_id()) + redraw_hwnds.append(hwnd) + try: + user32.SendMessageW(hwnd, WM_SETREDRAW, 0, 0) + except Exception: + pass hdwp = user32.DeferWindowPos(hdwp, hwnd, 0, int(x), int(y), 0, 0, flags) if not hdwp: + for redraw_hwnd in redraw_hwnds: + try: + user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0) + user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME) + except Exception: + pass return False - return bool(user32.EndDeferWindowPos(hdwp)) + ok = bool(user32.EndDeferWindowPos(hdwp)) + for redraw_hwnd in redraw_hwnds: + try: + user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0) + user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME) + except Exception: + pass + return ok except Exception: return False +def _restack_windows(windows: list[tk.Misc]) -> None: + """Lift windows from back to front without mixing movement and z-order updates.""" + + for child in windows: + try: + child.lift() + except Exception: + pass + + def place_window_below_parent(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0): """Place ``child`` so its outer top edge sits just below ``parent``. @@ -220,20 +393,95 @@ def place_window_fullsize_below_parent(parent: tk.Misc, child: tk.Misc, *, x_off parent.update_idletasks() child.update_idletasks() work_left, _work_top, work_right, work_bottom = _work_area_bounds(parent) + screen_h = int(parent.winfo_screenheight()) x = parent.winfo_x() + int(x_offset) y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap) - width = max(320, int(work_right) - int(x)) - height = max(240, int(work_bottom) - int(y)) + taskbar_h = _taskbar_thickness(parent) + usable_bottom = int(work_bottom) + if taskbar_h > 0: + usable_bottom = min(usable_bottom, screen_h - int(taskbar_h)) + extra_w, extra_h = _window_nonclient_extra(child) + width = max(320, int(work_right) - int(x) - int(extra_w)) + height = max(240, int(usable_bottom) - int(y) - int(extra_h)) + _MODULE_LOGGER.debug( + "fullsize.calc window=%s x=%s y=%s work_right=%s usable_bottom=%s taskbar=%s extra=(%s,%s) final=(%s,%s)", + _window_label(child), + x, + y, + work_right, + usable_bottom, + taskbar_h, + extra_w, + extra_h, + width, + height, + ) _set_window_bounds(child, x, y, width, height) except Exception: pass +def _fit_window_to_work_area(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0) -> None: + """Trim a rendered child window so its outer frame stays above the taskbar.""" + + try: + if not getattr(child, "winfo_exists", lambda: False)(): + return + parent.update_idletasks() + child.update_idletasks() + _work_left, _work_top, _work_right, work_bottom = _work_area_bounds(parent) + screen_h = int(parent.winfo_screenheight()) + taskbar_h = _taskbar_thickness(parent) + usable_bottom = int(work_bottom) + if taskbar_h > 0: + usable_bottom = min(usable_bottom, screen_h - int(taskbar_h)) + outer = _window_outer_bounds(child) + if outer is None: + return + left, top, right, bottom = outer + overflow = int(bottom) - int(usable_bottom) + _MODULE_LOGGER.debug( + "fit_window.check window=%s outer=(%s,%s,%s,%s) usable_bottom=%s overflow=%s", + _window_label(child), + left, + top, + right, + bottom, + usable_bottom, + overflow, + ) + if overflow <= 0: + return + current_w, current_h = _safe_wh(child) + if current_w is None or current_h is None: + return + new_h = max(240, int(current_h) - int(overflow) - 2) + x = parent.winfo_x() + int(x_offset) + y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap) + _MODULE_LOGGER.debug( + "fit_window.apply window=%s current=(%s,%s) new_h=%s", + _window_label(child), + current_w, + current_h, + new_h, + ) + _set_window_bounds(child, x, y, int(current_w), int(new_h)) + except Exception: + _MODULE_LOGGER.exception("fit_window.error") + + def place_window_fullsize_below_parent_later(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0): """Schedule full-size placement below the launcher after geometry settles.""" try: + try: + _set_window_alpha(child, 0.0) + except Exception: + pass child.after(0, lambda: place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap)) + child.after(120, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap)) + child.after(260, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap)) + child.after(300, lambda: _set_window_alpha(child, 1.0) if getattr(child, "winfo_exists", lambda: False)() else None) except Exception: place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap) @@ -324,11 +572,20 @@ def cascade_children_below_parent( try: if not batched: _set_window_position(child, x, y) - child.after(110, lambda w=child, px=x, py=y: _set_window_position(w, px, py) if getattr(w, "winfo_exists", lambda: False)() else None) except Exception: pass + try: + parent.after(10, lambda wins=list(windows): _restack_windows([w for w in wins if getattr(w, "winfo_exists", lambda: False)()])) + except Exception: + _restack_windows(windows) + for child, (x, y) in zip(windows, positions): try: - child.lift() + child.after( + 110, + lambda w=child, px=x, py=y: _ensure_window_position(w, px, py) + if getattr(w, "winfo_exists", lambda: False)() + else None, + ) except Exception: pass except Exception: