Milestone ultima alpha
This commit is contained in:
588
analisi_bug_prenotazione_pickinglist.md
Normal file
588
analisi_bug_prenotazione_pickinglist.md
Normal 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
481
barcode_client.py
Normal 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
153
barcode_repository.py
Normal 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
247
barcode_service.py
Normal 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
427
db_config.py
Normal 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
115
diagramma_scarico_udc.md
Normal 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
78
flussi operativi.txt
Normal 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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
197
locale.json
Normal 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
41
locale_text.py
Normal 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)
|
||||
143
login_window.py
143
login_window.py
@@ -7,7 +7,10 @@ from tkinter import messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from audit_log import log_session_event
|
||||
from busy_overlay import InlineBusyOverlay
|
||||
from gestione_aree import AsyncRunner
|
||||
from locale_text import load_locale_catalog, text as loc_text
|
||||
from ui_theme import theme_section, theme_value
|
||||
from user_session import UserSession, create_user_session
|
||||
|
||||
|
||||
@@ -48,17 +51,21 @@ def _rows_to_dicts(res: Any) -> list[dict[str, Any]]:
|
||||
class LoginWindow(tk.Toplevel):
|
||||
"""Small modal dialog used to authenticate one warehouse operator."""
|
||||
|
||||
def __init__(self, parent: tk.Misc, db_client):
|
||||
def __init__(self, parent: tk.Misc, db_client, *, compact: bool = False):
|
||||
super().__init__(parent)
|
||||
self.db_client = db_client
|
||||
self.compact = bool(compact)
|
||||
self.result_session: UserSession | None = None
|
||||
self._async = AsyncRunner(self)
|
||||
self._theme = theme_section("login_window", {})
|
||||
self._locale_catalog = load_locale_catalog()
|
||||
self._login_button: ttk.Button | None = None
|
||||
self._cancel_button: ttk.Button | None = None
|
||||
self._status_var = tk.StringVar(value="")
|
||||
self._busy = InlineBusyOverlay(self, self._theme)
|
||||
|
||||
self.title("Login Warehouse")
|
||||
self.geometry("420x250")
|
||||
self.title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"))
|
||||
self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "420x250")))
|
||||
self.resizable(False, False)
|
||||
try:
|
||||
if parent is not None and parent.winfo_viewable():
|
||||
@@ -81,45 +88,85 @@ class LoginWindow(tk.Toplevel):
|
||||
def _build_ui(self) -> None:
|
||||
"""Build the compact operator login form."""
|
||||
|
||||
body = ttk.Frame(self, padding=12)
|
||||
body = ttk.Frame(self, padding=8 if self.compact else 12)
|
||||
body.pack(fill="both", expand=True)
|
||||
body.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(
|
||||
body,
|
||||
text="Autenticazione operatore",
|
||||
font=("Segoe UI", 11, "bold"),
|
||||
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(4, 14))
|
||||
row_offset = 0
|
||||
if not self.compact:
|
||||
ttk.Label(
|
||||
body,
|
||||
text=loc_text("login.heading", catalog=self._locale_catalog, default="Autenticazione operatore"),
|
||||
font=("Segoe UI", 11, "bold"),
|
||||
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(4, 14))
|
||||
row_offset = 1
|
||||
|
||||
ttk.Label(body, text="Login").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6)
|
||||
self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=28)
|
||||
self.login_entry.grid(row=1, column=1, sticky="ew", pady=6)
|
||||
|
||||
ttk.Label(body, text="Password").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=6)
|
||||
self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=28, show="*")
|
||||
self.password_entry.grid(row=2, column=1, sticky="ew", pady=6)
|
||||
|
||||
self.info_label = ttk.Label(
|
||||
body,
|
||||
text="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.",
|
||||
justify="left",
|
||||
wraplength=320,
|
||||
ttk.Label(body, text="User:").grid(
|
||||
row=row_offset, column=0, sticky="w", padx=(0, 6), pady=4 if self.compact else 6
|
||||
)
|
||||
self.info_label.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 8))
|
||||
self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9))
|
||||
self.login_entry.grid(row=row_offset, column=1, sticky="ew", pady=4 if self.compact else 6)
|
||||
|
||||
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
|
||||
self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(2, 2))
|
||||
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 6), pady=4 if self.compact else 6)
|
||||
self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9))
|
||||
self.password_entry.grid(row=row_offset + 1, column=1, sticky="ew", pady=4 if self.compact else 6)
|
||||
|
||||
actions = ttk.Frame(body)
|
||||
actions.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(6, 0))
|
||||
actions.columnconfigure(0, weight=1)
|
||||
self._cancel_button = ttk.Button(actions, text="Annulla", command=self._on_cancel)
|
||||
self._cancel_button.grid(row=0, column=1, padx=(0, 8), pady=8)
|
||||
self._login_button = ttk.Button(actions, text="Accedi", command=self._on_login)
|
||||
self._login_button.grid(row=0, column=2, pady=8)
|
||||
if self.compact:
|
||||
actions = ttk.Frame(body)
|
||||
actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0))
|
||||
self._cancel_button = ttk.Button(
|
||||
actions,
|
||||
text="Annulla",
|
||||
command=self._on_cancel,
|
||||
)
|
||||
self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0))
|
||||
self._login_button = ttk.Button(
|
||||
actions,
|
||||
text="OK",
|
||||
command=self._on_login,
|
||||
)
|
||||
self._login_button.grid(row=0, column=0, sticky="ew")
|
||||
else:
|
||||
self.info_label = ttk.Label(
|
||||
body,
|
||||
text=loc_text(
|
||||
"login.info",
|
||||
catalog=self._locale_catalog,
|
||||
default="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.",
|
||||
),
|
||||
justify="left",
|
||||
wraplength=320,
|
||||
)
|
||||
self.info_label.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 8))
|
||||
|
||||
self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
|
||||
self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(2, 2))
|
||||
|
||||
actions = ttk.Frame(body)
|
||||
actions.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(6, 0))
|
||||
actions.columnconfigure(0, weight=1)
|
||||
self._cancel_button = ttk.Button(
|
||||
actions,
|
||||
text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"),
|
||||
command=self._on_cancel,
|
||||
)
|
||||
self._cancel_button.grid(row=0, column=1, padx=(0, 8), pady=8)
|
||||
self._login_button = ttk.Button(
|
||||
actions,
|
||||
text=loc_text("login.button.submit", catalog=self._locale_catalog, default="Accedi"),
|
||||
command=self._on_login,
|
||||
)
|
||||
self._login_button.grid(row=0, column=2, pady=8)
|
||||
|
||||
self.bind("<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
|
||||
|
||||
118
main.py
118
main.py
@@ -15,29 +15,28 @@ import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
|
||||
from async_loop_singleton import get_global_loop
|
||||
from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn
|
||||
from async_msssql_query import AsyncMSSQLClient
|
||||
from audit_log import log_session_event
|
||||
from db_config import build_dsn_from_config, ensure_db_config
|
||||
from gestione_layout import open_layout_window
|
||||
from gestione_pickinglist import open_pickinglist_window
|
||||
from login_window import prompt_login
|
||||
from locale_text import load_locale_catalog, text as loc_text
|
||||
from reset_corsie import open_reset_corsie_window
|
||||
from search_pallets import open_search_window
|
||||
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
|
||||
from ui_theme import theme_font, theme_section, theme_value
|
||||
from user_session import UserSession, create_user_session
|
||||
from view_celle_multi_udc import open_celle_multiple_window
|
||||
from window_placement import cascade_children_below_parent, place_window_fullsize_below_parent_later
|
||||
|
||||
|
||||
# ---- Config ----
|
||||
SERVER = r"mde3\gesterp"
|
||||
DBNAME = "Mediseawall"
|
||||
USER = "sa"
|
||||
PASSWORD = "1Password1"
|
||||
from window_placement import (
|
||||
cascade_children_below_parent,
|
||||
place_window_below_parent_later,
|
||||
place_window_fullsize_below_parent_later,
|
||||
)
|
||||
|
||||
# Development shortcut: skip the login dialog and boot directly as MAG1.
|
||||
# Set to False when you want to restore normal authentication.
|
||||
BYPASS_LOGIN = True
|
||||
BYPASS_LOGIN = False
|
||||
BYPASS_LOGIN_USER = {
|
||||
"operator_id": 4,
|
||||
"login": "MAG1",
|
||||
@@ -67,8 +66,7 @@ if not hasattr(tk.Toplevel, "block_update_dimensions_event"):
|
||||
if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
|
||||
tk.Toplevel.unblock_update_dimensions_event = _noop # type: ignore[attr-defined]
|
||||
|
||||
dsn_app = make_mssql_dsn(server=SERVER, database=DBNAME, user=USER, password=PASSWORD)
|
||||
db_app = AsyncMSSQLClient(dsn_app)
|
||||
db_app: AsyncMSSQLClient | None = None
|
||||
_APP_MUTEX = None
|
||||
_APP_MUTEX_NAME = "Local\\WarehousePythonAppSingleton"
|
||||
|
||||
@@ -114,18 +112,22 @@ class Launcher(ctk.CTk):
|
||||
"pickinglist",
|
||||
]
|
||||
|
||||
def __init__(self, session: UserSession):
|
||||
def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
|
||||
"""Create the launcher toolbar and wire every button to a feature window."""
|
||||
super().__init__()
|
||||
self.session: UserSession = session
|
||||
self.db_client = db_client
|
||||
self._theme = theme_section("launcher", {})
|
||||
self._locale_catalog = load_locale_catalog()
|
||||
self._tooltip_catalog = load_tooltip_catalog()
|
||||
self._child_windows: list[tk.Misc] = []
|
||||
self._child_windows_by_key: dict[str, tk.Misc] = {}
|
||||
self._is_cascading = False
|
||||
self._focus_restore_pending: set[str] = set()
|
||||
self._restore_suppressed_until = 0.0
|
||||
self.title(f"Warehouse 1.0.0 - {self.session.display_name}")
|
||||
self.title(
|
||||
f"{loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse 1.0.0')} - {self.session.display_name}"
|
||||
)
|
||||
self._apply_dynamic_geometry()
|
||||
|
||||
outer_pady = int(theme_value(self._theme, "outer_pady", 10))
|
||||
@@ -142,58 +144,58 @@ class Launcher(ctk.CTk):
|
||||
actions = [
|
||||
(
|
||||
"reset_corsie",
|
||||
"Gestione Corsie",
|
||||
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
|
||||
"launcher.open_reset_corsie",
|
||||
lambda: self._open_child_window(
|
||||
"reset_corsie",
|
||||
open_reset_corsie_window(self, db_app, session=self.session),
|
||||
open_reset_corsie_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"layout",
|
||||
"Gestione Layout",
|
||||
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
|
||||
"launcher.open_layout",
|
||||
lambda: self._open_child_window(
|
||||
"layout",
|
||||
open_layout_window(self, db_app, session=self.session),
|
||||
open_layout_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"multi_udc",
|
||||
"UDC Fantasma",
|
||||
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
|
||||
"launcher.open_multi_udc",
|
||||
lambda: self._open_child_window(
|
||||
"multi_udc",
|
||||
open_celle_multiple_window(self, db_app, session=self.session),
|
||||
open_celle_multiple_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"search",
|
||||
"Ricerca UDC",
|
||||
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
|
||||
"launcher.open_search",
|
||||
lambda: self._open_child_window(
|
||||
"search",
|
||||
open_search_window(self, db_app, session=self.session),
|
||||
open_search_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"pickinglist",
|
||||
"Gestione Picking List",
|
||||
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
|
||||
"launcher.open_pickinglist",
|
||||
lambda: self._open_child_window(
|
||||
"pickinglist",
|
||||
open_pickinglist_window(self, db_app, session=self.session),
|
||||
open_pickinglist_window(self, self.db_client, session=self.session),
|
||||
),
|
||||
),
|
||||
(
|
||||
"arrange",
|
||||
"Ridisponi finestre",
|
||||
loc_text("launcher.arrange", catalog=self._locale_catalog, default="Ridisponi finestre"),
|
||||
"launcher.arrange_windows",
|
||||
self._cascade_open_windows,
|
||||
),
|
||||
(
|
||||
"exit",
|
||||
"Esci",
|
||||
loc_text("launcher.exit", catalog=self._locale_catalog, default="Esci"),
|
||||
"launcher.exit",
|
||||
self._shutdown,
|
||||
),
|
||||
@@ -203,7 +205,11 @@ class Launcher(ctk.CTk):
|
||||
|
||||
info = ctk.CTkLabel(
|
||||
wrap,
|
||||
text=f"Operatore: {self.session.display_name} ({self.session.login})",
|
||||
text=loc_text(
|
||||
"launcher.operator",
|
||||
catalog=self._locale_catalog,
|
||||
default="Operatore: {display_name} ({login})",
|
||||
).format(display_name=self.session.display_name, login=self.session.login),
|
||||
anchor="w",
|
||||
font=theme_font(self._theme, "info_font", default=("Segoe UI", 12, "bold")),
|
||||
)
|
||||
@@ -288,18 +294,6 @@ class Launcher(ctk.CTk):
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
window.bind(
|
||||
"<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,11 +411,12 @@ class Launcher(ctk.CTk):
|
||||
try:
|
||||
if self.session is not None:
|
||||
log_session_event(self.session, action="logout", outcome="ok")
|
||||
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
||||
try:
|
||||
fut.result(timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
if self.db_client is not None:
|
||||
fut = asyncio.run_coroutine_threadsafe(self.db_client.dispose(), _loop)
|
||||
try:
|
||||
fut.result(timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.destroy()
|
||||
|
||||
@@ -428,8 +428,11 @@ if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
messagebox.showwarning(
|
||||
"Warehouse",
|
||||
"L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.",
|
||||
loc_text("launcher.already_running_title", default="Warehouse"),
|
||||
loc_text(
|
||||
"launcher.already_running_message",
|
||||
default="L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.",
|
||||
),
|
||||
parent=root,
|
||||
)
|
||||
try:
|
||||
@@ -438,6 +441,12 @@ if __name__ == "__main__":
|
||||
pass
|
||||
raise SystemExit(0)
|
||||
|
||||
db_cfg = ensure_db_config(_loop)
|
||||
if db_cfg is None:
|
||||
raise SystemExit(0)
|
||||
|
||||
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
|
||||
|
||||
if BYPASS_LOGIN:
|
||||
session = _build_bypass_session()
|
||||
log_session_event(
|
||||
@@ -457,11 +466,12 @@ if __name__ == "__main__":
|
||||
session = prompt_login(bootstrap, db_app)
|
||||
|
||||
if session is None:
|
||||
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
||||
try:
|
||||
fut.result(timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
if db_app is not None:
|
||||
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
||||
try:
|
||||
fut.result(timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if bootstrap is not None:
|
||||
bootstrap.destroy()
|
||||
@@ -475,4 +485,4 @@ if __name__ == "__main__":
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Launcher(session).mainloop()
|
||||
Launcher(session, db_app).mainloop()
|
||||
|
||||
119
patch_sp_xExePackingListPallet.sql
Normal file
119
patch_sp_xExePackingListPallet.sql
Normal 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
|
||||
@@ -254,92 +254,49 @@ async def _execute(db, sql: str, params: Dict[str, Any]) -> int:
|
||||
|
||||
|
||||
@_log_call()
|
||||
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) -> SPResult:
|
||||
"""Toggle the reservation state of all cells belonging to a packing list.
|
||||
|
||||
The implementation mirrors the original SQL stored procedure while using
|
||||
the shared async DB client already managed by the application.
|
||||
"""
|
||||
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult:
|
||||
"""Execute the original reservation stored procedure used by the C# client."""
|
||||
try:
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura async packing list avviata documento={Documento} id_operatore={IDOperatore}")
|
||||
nominativo = await _query_one_value(
|
||||
db,
|
||||
"SELECT LOGIN FROM Operatori WHERE id = :IDOperatore",
|
||||
{"IDOperatore": IDOperatore},
|
||||
) or ""
|
||||
|
||||
celle = await _query_all(
|
||||
db,
|
||||
"""
|
||||
SELECT DISTINCT Cella
|
||||
FROM dbo.XMag_ViewPackingList
|
||||
WHERE Documento = :Documento
|
||||
""",
|
||||
{"Documento": Documento},
|
||||
azione = str(Azione or "P").strip().upper()
|
||||
if azione not in ("P", "S"):
|
||||
return SPResult(rc=-10, message=f"Azione non valida: {Azione}", id_result=None)
|
||||
_MODULE_LOGGER.log(
|
||||
_MODULE_LOG_LEVEL,
|
||||
f"Procedura packing list via stored procedure documento={Documento} azione={azione} id_operatore={IDOperatore}",
|
||||
)
|
||||
id_celle = [row.get("Cella") for row in celle if "Cella" in row]
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Celle coinvolte per documento={Documento}: {len(id_celle)}")
|
||||
sql = """
|
||||
SET NOCOUNT ON;
|
||||
DECLARE @RC int = 0;
|
||||
|
||||
# Each cell is toggled individually because the original procedure also
|
||||
# updates metadata such as operator and timestamp per row.
|
||||
for id_cella in id_celle:
|
||||
if id_cella is None:
|
||||
continue
|
||||
stato = await _query_one_value(
|
||||
db,
|
||||
"SELECT IDStato FROM Celle WHERE ID = :IDC",
|
||||
{"IDC": id_cella},
|
||||
)
|
||||
_MODULE_LOGGER.debug(f"Toggling cella id={id_cella} stato_corrente={stato}")
|
||||
if stato == 0:
|
||||
await _execute(
|
||||
db,
|
||||
"""
|
||||
UPDATE Celle
|
||||
SET IDStato = 1,
|
||||
ModUtente = :N,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ID = :IDC
|
||||
""",
|
||||
{"N": nominativo, "IDC": id_cella},
|
||||
)
|
||||
else:
|
||||
await _execute(
|
||||
db,
|
||||
"""
|
||||
UPDATE Celle
|
||||
SET IDStato = 0,
|
||||
ModUtente = :N,
|
||||
ModDataOra = GETDATE()
|
||||
WHERE ID = :IDC
|
||||
""",
|
||||
{"N": nominativo, "IDC": id_cella},
|
||||
)
|
||||
EXEC dbo.sp_xExePackingListPallet
|
||||
@IDOperatore = :IDOperatore,
|
||||
@Documento = :Documento,
|
||||
@Azione = :Azione,
|
||||
@RC = @RC OUTPUT;
|
||||
|
||||
description = await _query_one_value(
|
||||
db,
|
||||
"""
|
||||
SELECT TOP 1 NAZIONE
|
||||
FROM dbo.XMag_ViewPackingList
|
||||
WHERE Documento = :Documento
|
||||
GROUP BY Documento, NAZIONE
|
||||
ORDER BY NAZIONE
|
||||
""",
|
||||
{"Documento": Documento},
|
||||
SELECT CAST(@RC AS int) AS RC;
|
||||
"""
|
||||
_log_sql("sp_xExePackingListPallet", sql, {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione})
|
||||
if not hasattr(db, "query_json"):
|
||||
raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.")
|
||||
res = await db.query_json(
|
||||
sql,
|
||||
{"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione},
|
||||
as_dict_rows=True,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
await _execute(
|
||||
db,
|
||||
"""
|
||||
INSERT INTO dbo.LogPackingList (Code, Description, IDInsUser, InsDateTime)
|
||||
VALUES (:Code, :Descr, :IDInsUser, GETDATE());
|
||||
""",
|
||||
{"Code": Documento, "Descr": description, "IDInsUser": IDOperatore},
|
||||
)
|
||||
|
||||
new_id = await _query_one_value(db, "SELECT SCOPE_IDENTITY() AS ID", {})
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura completata documento={Documento} id_result={new_id}")
|
||||
return SPResult(rc=0, message="", id_result=int(new_id) if new_id is not None else None)
|
||||
rows = []
|
||||
if isinstance(res, dict):
|
||||
rows = res.get("rows", []) or []
|
||||
_log_dataset("sp_xExePackingListPallet", rows)
|
||||
rc = 0
|
||||
if rows and isinstance(rows[0], dict):
|
||||
try:
|
||||
rc = int(rows[0].get("RC") or 0)
|
||||
except Exception:
|
||||
rc = 0
|
||||
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Stored procedure completata documento={Documento} azione={azione} rc={rc}")
|
||||
return SPResult(rc=rc, message="", id_result=None)
|
||||
except Exception as exc:
|
||||
_MODULE_LOGGER.exception(f"Procedura fallita documento={Documento}: {exc}")
|
||||
_MODULE_LOGGER.exception(f"Procedura fallita documento={Documento} azione={Azione}: {exc}")
|
||||
return SPResult(rc=-1, message=str(exc), id_result=None)
|
||||
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
aioodbc
|
||||
customtkinter
|
||||
loguru
|
||||
openpyxl
|
||||
orjson
|
||||
pyodbc
|
||||
SQLAlchemy
|
||||
tksheet
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
383
spec_barcode_wms.rtf
Normal 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}}
|
||||
170
spec_barcode_wms_aggiornata.rtf
Normal file
170
spec_barcode_wms_aggiornata.rtf
Normal 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
|
||||
}
|
||||
24
tooltip.json
24
tooltip.json
@@ -8,6 +8,18 @@
|
||||
"launcher.open_pickinglist": "Apre la gestione delle picking list per prenotare, controllare e aggiornare le liste di prelievo.",
|
||||
"launcher.arrange_windows": "Dispone in cascata le finestre aperte seguendo l'ordine dei pulsanti del launcher.",
|
||||
"launcher.exit": "Chiude l'applicazione in modo pulito terminando la sessione utente e rilasciando la connessione condivisa al database.",
|
||||
"dbconfig.heading": "Spiega che qui si inseriscono i dati minimi per permettere al programma di collegarsi al database del magazzino al primo avvio.",
|
||||
"dbconfig.field.server": "Nome server o indirizzo IP dell'istanza SQL Server che ospita il database del magazzino.",
|
||||
"dbconfig.field.database": "Nome del database applicativo da usare per il WMS.",
|
||||
"dbconfig.field.user": "Utente SQL Server da usare per l'accesso al database.",
|
||||
"dbconfig.field.password": "Password dell'utente SQL Server configurato per il collegamento.",
|
||||
"dbconfig.field.driver": "Driver ODBC installato sul PC che verra' usato da Python per collegarsi a SQL Server.",
|
||||
"dbconfig.field.encrypt": "Valore opzionale del parametro Encrypt. Lascialo vuoto se non serve una configurazione specifica.",
|
||||
"dbconfig.field.trust_server_certificate": "Attiva la fiducia sul certificato del server SQL quando l'ambiente usa certificati locali o autofirmati.",
|
||||
"dbconfig.info": "Il file di configurazione viene salvato una sola volta sul PC e riutilizzato ai successivi avvii del programma.",
|
||||
"dbconfig.button.cancel": "Chiude la configurazione iniziale senza salvare. In questo caso il programma non prosegue.",
|
||||
"dbconfig.button.test": "Prova subito la connessione con i dati inseriti senza ancora salvarli definitivamente.",
|
||||
"dbconfig.button.save": "Verifica i dati, salva il file di configurazione e permette al programma di continuare l'avvio.",
|
||||
"reset_corsie.refresh": "Ricarica il riepilogo e l'elenco delle celle occupate per la corsia selezionata.",
|
||||
"reset_corsie.empty_aisle": "Scarica logicamente tutte le UDC attive della corsia selezionata verso l'ubicazione di uscita.",
|
||||
"layout.search_udc": "Cerca una UDC per barcode, cambia automaticamente corsia e porta in evidenza la cella trovata.",
|
||||
@@ -28,6 +40,18 @@
|
||||
"launcher.open_pickinglist": "Open picking list management to reserve, inspect and update picking lists.",
|
||||
"launcher.arrange_windows": "Arrange open windows in cascade order following the launcher buttons.",
|
||||
"launcher.exit": "Close the application cleanly by ending the user session and releasing the shared database connection.",
|
||||
"dbconfig.heading": "Explain that this form collects the minimum data needed to connect the program to the warehouse database on first startup.",
|
||||
"dbconfig.field.server": "SQL Server instance name or IP address hosting the warehouse database.",
|
||||
"dbconfig.field.database": "Application database name used by the WMS.",
|
||||
"dbconfig.field.user": "SQL Server user used to access the database.",
|
||||
"dbconfig.field.password": "Password for the configured SQL Server user.",
|
||||
"dbconfig.field.driver": "ODBC driver installed on the PC that Python will use to connect to SQL Server.",
|
||||
"dbconfig.field.encrypt": "Optional Encrypt parameter value. Leave it blank if no specific encryption setting is required.",
|
||||
"dbconfig.field.trust_server_certificate": "Enable trust for the SQL Server certificate when the environment uses local or self-signed certificates.",
|
||||
"dbconfig.info": "The configuration file is saved once on the PC and reused on the next application startups.",
|
||||
"dbconfig.button.cancel": "Close the first-run configuration without saving. In this case the program will not continue.",
|
||||
"dbconfig.button.test": "Try the connection immediately with the entered values without saving them yet.",
|
||||
"dbconfig.button.save": "Validate the connection, save the configuration file and let the program continue startup.",
|
||||
"reset_corsie.refresh": "Reload the summary and the list of occupied cells for the selected aisle.",
|
||||
"reset_corsie.empty_aisle": "Logically unload all active UDCs in the selected aisle to the outbound location.",
|
||||
"layout.search_udc": "Search a UDC by barcode, switch aisle automatically and highlight the matching cell.",
|
||||
|
||||
106
ui_theme.json
106
ui_theme.json
@@ -161,5 +161,111 @@
|
||||
"size": 12,
|
||||
"weight": "bold"
|
||||
}
|
||||
},
|
||||
"login_window": {
|
||||
"window_geometry": "420x250",
|
||||
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
|
||||
"overlay_panel_fg_color": ["#f2f2f2", "#353535"],
|
||||
"overlay_panel_corner_radius": 10,
|
||||
"overlay_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 11,
|
||||
"weight": "bold"
|
||||
},
|
||||
"overlay_progress_width": 220,
|
||||
"overlay_label_padding": [18, 14, 18, 8],
|
||||
"overlay_progress_padding": [18, 0, 18, 14]
|
||||
},
|
||||
"search_window": {
|
||||
"window_geometry": "1100x720",
|
||||
"window_minsize": [900, 560],
|
||||
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||
"toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||
"toolbar_button_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"toolbar_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"entry_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
|
||||
"overlay_panel_fg_color": ["#f2f2f2", "#353535"],
|
||||
"overlay_panel_corner_radius": 10,
|
||||
"overlay_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 11,
|
||||
"weight": "bold"
|
||||
},
|
||||
"overlay_progress_width": 220,
|
||||
"overlay_label_padding": [18, 14, 18, 8],
|
||||
"overlay_progress_padding": [18, 0, 18, 14]
|
||||
},
|
||||
"scarico_dialog": {
|
||||
"header_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 13,
|
||||
"weight": "bold"
|
||||
},
|
||||
"body_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"button_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"tree_heading_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"tree_body_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "normal"
|
||||
},
|
||||
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
|
||||
"overlay_panel_fg_color": ["#f2f2f2", "#353535"],
|
||||
"overlay_panel_corner_radius": 10,
|
||||
"overlay_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 11,
|
||||
"weight": "bold"
|
||||
},
|
||||
"overlay_progress_width": 220,
|
||||
"overlay_label_padding": [18, 14, 18, 8],
|
||||
"overlay_progress_padding": [18, 0, 18, 14]
|
||||
},
|
||||
"pickinglist_window": {
|
||||
"window_geometry": "1200x700",
|
||||
"window_minsize": [1000, 560],
|
||||
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||
"toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||
"toolbar_button_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 10,
|
||||
"weight": "bold"
|
||||
},
|
||||
"overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"],
|
||||
"overlay_panel_fg_color": ["#f2f2f2", "#353535"],
|
||||
"overlay_panel_corner_radius": 10,
|
||||
"overlay_label_font": {
|
||||
"family": "Segoe UI",
|
||||
"size": 11,
|
||||
"weight": "bold"
|
||||
},
|
||||
"overlay_progress_width": 220,
|
||||
"overlay_label_padding": [18, 14, 18, 8],
|
||||
"overlay_progress_padding": [18, 0, 18, 14]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -76,6 +76,129 @@ def _work_area_bounds(window: tk.Misc) -> tuple[int, int, int, int]:
|
||||
return 0, 0, int(window.winfo_screenwidth()), int(window.winfo_screenheight())
|
||||
|
||||
|
||||
def _taskbar_thickness(window: tk.Misc) -> int:
|
||||
"""Return the Windows taskbar thickness when it can be determined."""
|
||||
|
||||
try:
|
||||
screen_h = int(window.winfo_screenheight())
|
||||
_left, _top, _right, work_bottom = _work_area_bounds(window)
|
||||
inferred = max(0, screen_h - int(work_bottom))
|
||||
if inferred > 0:
|
||||
return inferred
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if hasattr(ctypes, "windll"):
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("left", ctypes.c_long),
|
||||
("top", ctypes.c_long),
|
||||
("right", ctypes.c_long),
|
||||
("bottom", ctypes.c_long),
|
||||
]
|
||||
|
||||
class APPBARDATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", ctypes.c_uint),
|
||||
("hWnd", ctypes.c_void_p),
|
||||
("uCallbackMessage", ctypes.c_uint),
|
||||
("uEdge", ctypes.c_uint),
|
||||
("rc", RECT),
|
||||
("lParam", ctypes.c_long),
|
||||
]
|
||||
|
||||
ABM_GETTASKBARPOS = 0x00000005
|
||||
abd = APPBARDATA()
|
||||
abd.cbSize = ctypes.sizeof(APPBARDATA)
|
||||
if ctypes.windll.shell32.SHAppBarMessage(ABM_GETTASKBARPOS, ctypes.byref(abd)):
|
||||
rect = abd.rc
|
||||
width = max(0, int(rect.right) - int(rect.left))
|
||||
height = max(0, int(rect.bottom) - int(rect.top))
|
||||
return max(width, height)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _window_nonclient_extra(window: tk.Misc) -> tuple[int, int]:
|
||||
"""Return extra outer frame size added by Windows around the client area."""
|
||||
|
||||
try:
|
||||
if not hasattr(ctypes, "windll"):
|
||||
return 0, 0
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("left", ctypes.c_long),
|
||||
("top", ctypes.c_long),
|
||||
("right", ctypes.c_long),
|
||||
("bottom", ctypes.c_long),
|
||||
]
|
||||
|
||||
class POINT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_long),
|
||||
("y", ctypes.c_long),
|
||||
]
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = int(window.winfo_id())
|
||||
outer = RECT()
|
||||
client = RECT()
|
||||
origin = POINT()
|
||||
if not user32.GetWindowRect(hwnd, ctypes.byref(outer)):
|
||||
return 0, 0
|
||||
if not user32.GetClientRect(hwnd, ctypes.byref(client)):
|
||||
return 0, 0
|
||||
if not user32.ClientToScreen(hwnd, ctypes.byref(origin)):
|
||||
return 0, 0
|
||||
|
||||
outer_w = max(0, int(outer.right) - int(outer.left))
|
||||
outer_h = max(0, int(outer.bottom) - int(outer.top))
|
||||
client_w = max(0, int(client.right) - int(client.left))
|
||||
client_h = max(0, int(client.bottom) - int(client.top))
|
||||
extra_w = max(0, outer_w - client_w)
|
||||
extra_h = max(0, outer_h - client_h)
|
||||
return extra_w, extra_h
|
||||
except Exception:
|
||||
return 0, 0
|
||||
|
||||
|
||||
def _window_outer_bounds(window: tk.Misc) -> tuple[int, int, int, int] | None:
|
||||
"""Return the actual outer window rect in screen coordinates."""
|
||||
|
||||
try:
|
||||
if not hasattr(ctypes, "windll"):
|
||||
return None
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("left", ctypes.c_long),
|
||||
("top", ctypes.c_long),
|
||||
("right", ctypes.c_long),
|
||||
("bottom", ctypes.c_long),
|
||||
]
|
||||
|
||||
rect = RECT()
|
||||
hwnd = int(window.winfo_id())
|
||||
if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||||
return None
|
||||
return int(rect.left), int(rect.top), int(rect.right), int(rect.bottom)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _set_window_alpha(window: tk.Misc, alpha: float) -> None:
|
||||
"""Best-effort helper to change a window opacity."""
|
||||
|
||||
try:
|
||||
window.attributes("-alpha", float(alpha))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _set_window_bounds(child: tk.Misc, x: int, y: int, width: int | None = None, height: int | None = None) -> None:
|
||||
"""Move a toplevel to the requested bounds, resizing it when dimensions are provided."""
|
||||
|
||||
@@ -146,6 +269,21 @@ def _set_window_position(child: tk.Misc, x: int, y: int) -> None:
|
||||
_set_window_bounds(child, x, y, None, None)
|
||||
|
||||
|
||||
def _ensure_window_position(child: tk.Misc, x: int, y: int, *, tolerance: int = 2) -> None:
|
||||
"""Only correct the window position when it really drifted from the target."""
|
||||
|
||||
try:
|
||||
current_x, current_y = _safe_xy(child)
|
||||
if current_x is None or current_y is None:
|
||||
_set_window_position(child, x, y)
|
||||
return
|
||||
if abs(current_x - int(x)) <= tolerance and abs(current_y - int(y)) <= tolerance:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
_set_window_position(child, x, y)
|
||||
|
||||
|
||||
def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[int, int]]) -> bool:
|
||||
"""Try to reposition multiple windows in one Win32 batch to reduce flicker."""
|
||||
|
||||
@@ -156,6 +294,11 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in
|
||||
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
WM_SETREDRAW = 0x000B
|
||||
RDW_INVALIDATE = 0x0001
|
||||
RDW_ALLCHILDREN = 0x0080
|
||||
RDW_FRAME = 0x0400
|
||||
redraw_hwnds: list[int] = []
|
||||
hdwp = user32.BeginDeferWindowPos(len(windows))
|
||||
if not hdwp:
|
||||
return False
|
||||
@@ -163,7 +306,9 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in
|
||||
SWP_NOSIZE = 0x0001
|
||||
SWP_NOZORDER = 0x0004
|
||||
SWP_NOACTIVATE = 0x0010
|
||||
flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
|
||||
SWP_NOREDRAW = 0x0008
|
||||
SWP_DEFERERASE = 0x2000
|
||||
flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW | SWP_DEFERERASE
|
||||
|
||||
for child, (x, y) in zip(windows, positions):
|
||||
try:
|
||||
@@ -175,15 +320,43 @@ def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[in
|
||||
except Exception:
|
||||
pass
|
||||
hwnd = int(child.winfo_id())
|
||||
redraw_hwnds.append(hwnd)
|
||||
try:
|
||||
user32.SendMessageW(hwnd, WM_SETREDRAW, 0, 0)
|
||||
except Exception:
|
||||
pass
|
||||
hdwp = user32.DeferWindowPos(hdwp, hwnd, 0, int(x), int(y), 0, 0, flags)
|
||||
if not hdwp:
|
||||
for redraw_hwnd in redraw_hwnds:
|
||||
try:
|
||||
user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0)
|
||||
user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
return bool(user32.EndDeferWindowPos(hdwp))
|
||||
ok = bool(user32.EndDeferWindowPos(hdwp))
|
||||
for redraw_hwnd in redraw_hwnds:
|
||||
try:
|
||||
user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0)
|
||||
user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME)
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _restack_windows(windows: list[tk.Misc]) -> None:
|
||||
"""Lift windows from back to front without mixing movement and z-order updates."""
|
||||
|
||||
for child in windows:
|
||||
try:
|
||||
child.lift()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def place_window_below_parent(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
|
||||
"""Place ``child`` so its outer top edge sits just below ``parent``.
|
||||
|
||||
@@ -220,20 +393,95 @@ def place_window_fullsize_below_parent(parent: tk.Misc, child: tk.Misc, *, x_off
|
||||
parent.update_idletasks()
|
||||
child.update_idletasks()
|
||||
work_left, _work_top, work_right, work_bottom = _work_area_bounds(parent)
|
||||
screen_h = int(parent.winfo_screenheight())
|
||||
x = parent.winfo_x() + int(x_offset)
|
||||
y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
|
||||
width = max(320, int(work_right) - int(x))
|
||||
height = max(240, int(work_bottom) - int(y))
|
||||
taskbar_h = _taskbar_thickness(parent)
|
||||
usable_bottom = int(work_bottom)
|
||||
if taskbar_h > 0:
|
||||
usable_bottom = min(usable_bottom, screen_h - int(taskbar_h))
|
||||
extra_w, extra_h = _window_nonclient_extra(child)
|
||||
width = max(320, int(work_right) - int(x) - int(extra_w))
|
||||
height = max(240, int(usable_bottom) - int(y) - int(extra_h))
|
||||
_MODULE_LOGGER.debug(
|
||||
"fullsize.calc window=%s x=%s y=%s work_right=%s usable_bottom=%s taskbar=%s extra=(%s,%s) final=(%s,%s)",
|
||||
_window_label(child),
|
||||
x,
|
||||
y,
|
||||
work_right,
|
||||
usable_bottom,
|
||||
taskbar_h,
|
||||
extra_w,
|
||||
extra_h,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
_set_window_bounds(child, x, y, width, height)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _fit_window_to_work_area(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0) -> None:
|
||||
"""Trim a rendered child window so its outer frame stays above the taskbar."""
|
||||
|
||||
try:
|
||||
if not getattr(child, "winfo_exists", lambda: False)():
|
||||
return
|
||||
parent.update_idletasks()
|
||||
child.update_idletasks()
|
||||
_work_left, _work_top, _work_right, work_bottom = _work_area_bounds(parent)
|
||||
screen_h = int(parent.winfo_screenheight())
|
||||
taskbar_h = _taskbar_thickness(parent)
|
||||
usable_bottom = int(work_bottom)
|
||||
if taskbar_h > 0:
|
||||
usable_bottom = min(usable_bottom, screen_h - int(taskbar_h))
|
||||
outer = _window_outer_bounds(child)
|
||||
if outer is None:
|
||||
return
|
||||
left, top, right, bottom = outer
|
||||
overflow = int(bottom) - int(usable_bottom)
|
||||
_MODULE_LOGGER.debug(
|
||||
"fit_window.check window=%s outer=(%s,%s,%s,%s) usable_bottom=%s overflow=%s",
|
||||
_window_label(child),
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
usable_bottom,
|
||||
overflow,
|
||||
)
|
||||
if overflow <= 0:
|
||||
return
|
||||
current_w, current_h = _safe_wh(child)
|
||||
if current_w is None or current_h is None:
|
||||
return
|
||||
new_h = max(240, int(current_h) - int(overflow) - 2)
|
||||
x = parent.winfo_x() + int(x_offset)
|
||||
y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
|
||||
_MODULE_LOGGER.debug(
|
||||
"fit_window.apply window=%s current=(%s,%s) new_h=%s",
|
||||
_window_label(child),
|
||||
current_w,
|
||||
current_h,
|
||||
new_h,
|
||||
)
|
||||
_set_window_bounds(child, x, y, int(current_w), int(new_h))
|
||||
except Exception:
|
||||
_MODULE_LOGGER.exception("fit_window.error")
|
||||
|
||||
|
||||
def place_window_fullsize_below_parent_later(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
|
||||
"""Schedule full-size placement below the launcher after geometry settles."""
|
||||
|
||||
try:
|
||||
try:
|
||||
_set_window_alpha(child, 0.0)
|
||||
except Exception:
|
||||
pass
|
||||
child.after(0, lambda: place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap))
|
||||
child.after(120, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap))
|
||||
child.after(260, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap))
|
||||
child.after(300, lambda: _set_window_alpha(child, 1.0) if getattr(child, "winfo_exists", lambda: False)() else None)
|
||||
except Exception:
|
||||
place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap)
|
||||
|
||||
@@ -324,11 +572,20 @@ def cascade_children_below_parent(
|
||||
try:
|
||||
if not batched:
|
||||
_set_window_position(child, x, y)
|
||||
child.after(110, lambda w=child, px=x, py=y: _set_window_position(w, px, py) if getattr(w, "winfo_exists", lambda: False)() else None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
parent.after(10, lambda wins=list(windows): _restack_windows([w for w in wins if getattr(w, "winfo_exists", lambda: False)()]))
|
||||
except Exception:
|
||||
_restack_windows(windows)
|
||||
for child, (x, y) in zip(windows, positions):
|
||||
try:
|
||||
child.lift()
|
||||
child.after(
|
||||
110,
|
||||
lambda w=child, px=x, py=y: _ensure_window_position(w, px, py)
|
||||
if getattr(w, "winfo_exists", lambda: False)()
|
||||
else None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user