Milestone ultima alpha

This commit is contained in:
2026-05-22 14:25:09 +02:00
parent 8489cd7459
commit a5e704c214
25 changed files with 3896 additions and 273 deletions

View File

@@ -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 (<doc1>, <doc2>)
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`)

481
barcode_client.py Normal file
View File

@@ -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("<F1>", lambda _e: self._start_queue(1))
self.root.bind("<F2>", lambda _e: self._start_queue(0))
self.root.bind("<F4>", lambda _e: self._begin_manual_unload())
self.pallet_entry.bind("<Return>", self._on_pallet_enter)
self.destination_entry.bind("<Return>", 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())

153
barcode_repository.py Normal file
View File

@@ -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"]),
)

247
barcode_service.py Normal file
View File

@@ -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 "")

427
db_config.py Normal file
View File

@@ -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

115
diagramma_scarico_udc.md Normal file
View File

@@ -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

78
flussi operativi.txt Normal file
View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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(),

197
locale.json Normal file
View File

@@ -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}"
}
}

41
locale_text.py Normal file
View File

@@ -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)

View File

@@ -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,27 +88,52 @@ 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)
row_offset = 0
if not self.compact:
ttk.Label(
body,
text="Autenticazione operatore",
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="User:").grid(
row=row_offset, column=0, sticky="w", padx=(0, 6), pady=4 if self.compact else 6
)
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)
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)
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)
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="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.",
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,
)
@@ -113,13 +145,28 @@ class LoginWindow(tk.Toplevel):
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 = 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="Accedi", command=self._on_login)
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("<Return>", lambda _e: self._on_login())
self.bind("<Escape>", 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

100
main.py
View File

@@ -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(
"<ButtonPress-1>",
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,7 +411,8 @@ 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)
if self.db_client is not None:
fut = asyncio.run_coroutine_threadsafe(self.db_client.dispose(), _loop)
try:
fut.result(timeout=2)
except Exception:
@@ -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,6 +466,7 @@ if __name__ == "__main__":
session = prompt_login(bootstrap, db_app)
if session is None:
if db_app is not None:
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
try:
fut.result(timeout=2)
@@ -475,4 +485,4 @@ if __name__ == "__main__":
except Exception:
pass
Launcher(session).mainloop()
Launcher(session, db_app).mainloop()

View File

@@ -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

View File

@@ -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 ""
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}",
)
sql = """
SET NOCOUNT ON;
DECLARE @RC int = 0;
celle = await _query_all(
db,
"""
SELECT DISTINCT Cella
FROM dbo.XMag_ViewPackingList
WHERE Documento = :Documento
""",
{"Documento": Documento},
)
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)}")
EXEC dbo.sp_xExePackingListPallet
@IDOperatore = :IDOperatore,
@Documento = :Documento,
@Azione = :Azione,
@RC = @RC OUTPUT;
# 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,
SELECT CAST(@RC AS int) AS RC;
"""
UPDATE Celle
SET IDStato = 1,
ModUtente = :N,
ModDataOra = GETDATE()
WHERE ID = :IDC
""",
{"N": nominativo, "IDC": id_cella},
_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,
)
else:
await _execute(
db,
"""
UPDATE Celle
SET IDStato = 0,
ModUtente = :N,
ModDataOra = GETDATE()
WHERE ID = :IDC
""",
{"N": nominativo, "IDC": id_cella},
)
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},
)
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)

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
aioodbc
customtkinter
loguru
openpyxl
orjson
pyodbc
SQLAlchemy
tksheet

View File

@@ -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))

View File

@@ -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

383
spec_barcode_wms.rtf Normal file
View File

@@ -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}}

View File

@@ -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
}

View File

@@ -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.",

View File

@@ -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]
}
}

View File

@@ -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):

View File

@@ -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:
child.lift()
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.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: