Compare commits
6 Commits
f556b476ff
...
742f6a9fe9
| Author | SHA1 | Date | |
|---|---|---|---|
| 742f6a9fe9 | |||
| 4dabba8ce7 | |||
| d2a1f6a068 | |||
| a5e704c214 | |||
| 8489cd7459 | |||
| 6ab42a2303 |
61
INSTALLAZIONE.md
Normal file
61
INSTALLAZIONE.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Installazione Warehouse Python
|
||||||
|
|
||||||
|
## Requisiti
|
||||||
|
|
||||||
|
1. Windows 11 o Windows Server 2019.
|
||||||
|
2. Python 3.13 o versione compatibile installata e presente nel `PATH`.
|
||||||
|
3. Microsoft ODBC Driver per SQL Server installato sul PC.
|
||||||
|
- Driver consigliato: `ODBC Driver 17 for SQL Server`.
|
||||||
|
4. Accesso di rete al server SQL.
|
||||||
|
|
||||||
|
## Installazione dipendenze
|
||||||
|
|
||||||
|
Aprire PowerShell nella cartella del programma ed eseguire:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avvio backoffice
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Al primo avvio, se manca `db_connection.json`, il programma mostra la finestra di configurazione database.
|
||||||
|
|
||||||
|
Per avviare senza finestra console:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pythonw warehouse.pyw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avvio client barcode
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python barcode_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Per avviare senza finestra console:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pythonw barcode.pyw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Patch DB Picking List
|
||||||
|
|
||||||
|
Per usare in parallelo C# legacy e Python, eseguire in SSMS:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apply_python_parallel_pickinglist_patch.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Questa patch crea solo oggetti dedicati al Python con prefisso `py_` e non modifica gli oggetti legacy usati dal C#.
|
||||||
|
|
||||||
|
Per rimuovere gli oggetti Python dopo i test, eseguire:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rollback_python_parallel_pickinglist_patch.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
La vecchia patch `apply_plist_reservation_patch.sql` modifica invece gli oggetti legacy: usarla solo se si vuole sostituire temporaneamente il comportamento del C#.
|
||||||
821
analisi_bug_prenotazione_pickinglist.md
Normal file
821
analisi_bug_prenotazione_pickinglist.md
Normal file
@@ -0,0 +1,821 @@
|
|||||||
|
# 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`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Revisione dopo test reale su documenti 133 / 135 / 137
|
||||||
|
|
||||||
|
Dopo avere applicato la prima correzione proposta solo sulla stored, il bug **resta presente**:
|
||||||
|
|
||||||
|
- prenotando il documento `133`
|
||||||
|
- la picking list `135` continua a risultare prenotata
|
||||||
|
|
||||||
|
Questo comportamento è coerente con le evidenze raccolte:
|
||||||
|
|
||||||
|
- `133` e `135` condividono la cella `8057`
|
||||||
|
- `135` e `137` condividono la cella `1000`
|
||||||
|
|
||||||
|
### Conclusione aggiornata
|
||||||
|
|
||||||
|
La correzione che agisce **solo** su `Celle.IDStato` non è sufficiente.
|
||||||
|
|
||||||
|
Il motivo è strutturale:
|
||||||
|
|
||||||
|
1. la stored può anche rendere idempotente `Prenota` / `S-prenota`
|
||||||
|
2. ma se la UI e il barcode continuano a dedurre lo stato della prenotazione da `Celle.IDStato`
|
||||||
|
3. una cella condivisa continuerà a far risultare prenotati anche documenti diversi
|
||||||
|
|
||||||
|
Quindi il modello corretto deve spostare la **sorgente di verità** della prenotazione:
|
||||||
|
|
||||||
|
- **da:** stato della cella
|
||||||
|
- **a:** documento prenotato attivo
|
||||||
|
|
||||||
|
### Nuova architettura proposta
|
||||||
|
|
||||||
|
La soluzione corretta è questa:
|
||||||
|
|
||||||
|
1. introdurre una piccola tabella di stato, ad esempio `dbo.PickingListReservation`
|
||||||
|
2. salvare lì il **documento attualmente prenotato**
|
||||||
|
3. fare in modo che `XMag_ViewPackingList.IDStato` non legga più la prenotazione da `Celle.IDStato`
|
||||||
|
4. calcolare invece `IDStato` così:
|
||||||
|
- `1` se `Documento = documento prenotato attivo`
|
||||||
|
- `0` negli altri casi
|
||||||
|
|
||||||
|
In questo modo:
|
||||||
|
|
||||||
|
- il backoffice mostra prenotata una sola picking list
|
||||||
|
- il barcode continua a usare `F1` = `IDStato 1` e `F2` = `IDStato 0`
|
||||||
|
- le celle condivise `1000` / `8057` non contaminano più altri documenti
|
||||||
|
|
||||||
|
### Ruolo residuo di `Celle.IDStato`
|
||||||
|
|
||||||
|
`Celle.IDStato` può restare utile come supporto:
|
||||||
|
|
||||||
|
- per visualizzazioni legacy
|
||||||
|
- per evidenziare le celle del documento attivo
|
||||||
|
- per non rompere altre parti del sistema che si aspettano quel flag
|
||||||
|
|
||||||
|
Ma non deve più essere la **fonte primaria** della prenotazione documento.
|
||||||
|
|
||||||
|
### Stored definitiva da realizzare
|
||||||
|
|
||||||
|
La stored `sp_xExePackingListPallet` deve quindi:
|
||||||
|
|
||||||
|
1. mantenere il parametro `@Azione = 'P' | 'S'`
|
||||||
|
2. aggiornare la tabella `PickingListReservation`
|
||||||
|
3. opzionalmente riallineare anche `Celle.IDStato`
|
||||||
|
4. mantenere il log in `LogPackingList`
|
||||||
|
|
||||||
|
Semantica definitiva:
|
||||||
|
|
||||||
|
- `Prenota`
|
||||||
|
- se il documento è già quello attivo: nessun effetto
|
||||||
|
- altrimenti:
|
||||||
|
- il documento diventa l’unico prenotato attivo
|
||||||
|
- tutte le altre picking list risultano non prenotate
|
||||||
|
|
||||||
|
- `S-prenota`
|
||||||
|
- se il documento non è quello attivo: nessun effetto
|
||||||
|
- se il documento è quello attivo:
|
||||||
|
- la prenotazione viene rimossa
|
||||||
|
- nessun altro documento viene acceso automaticamente
|
||||||
|
|
||||||
|
### Nota importante
|
||||||
|
|
||||||
|
Questa revisione **supera** la proposta precedente basata soltanto sul reset/set di `Celle.IDStato`.
|
||||||
|
|
||||||
|
La patch SQL definitiva deve quindi includere:
|
||||||
|
|
||||||
|
1. creazione della tabella `PickingListReservation`
|
||||||
|
2. `CREATE OR ALTER` della stored `sp_xExePackingListPallet`
|
||||||
|
3. `CREATE OR ALTER` della vista `XMag_ViewPackingList`
|
||||||
|
|
||||||
|
Questo è il primo assetto realmente coerente con il requisito di business:
|
||||||
|
|
||||||
|
> una sola picking list prenotata alla volta, anche in presenza di celle condivise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evoluzione proposta: usare `ViewPackingListRestante` anche per la lista alta
|
||||||
|
|
||||||
|
### Situazione attuale
|
||||||
|
|
||||||
|
Nel legacy C# e nel Python attuale la distinzione è questa:
|
||||||
|
|
||||||
|
- la **griglia alta** legge la testata aggregata da `XMag_ViewPackingList`
|
||||||
|
- la **griglia bassa** legge il dettaglio da `ViewPackingListRestante`
|
||||||
|
|
||||||
|
La vista `ViewPackingListRestante` è definita come:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT ...
|
||||||
|
FROM XMag_ViewPackingList
|
||||||
|
WHERE Cella <> 9999
|
||||||
|
```
|
||||||
|
|
||||||
|
Quindi:
|
||||||
|
|
||||||
|
- `XMag_ViewPackingList` contiene anche le UDC già finite nella locazione convenzionale `9999 = 7G.1.1`
|
||||||
|
- `ViewPackingListRestante` mostra invece solo le UDC ancora residue
|
||||||
|
|
||||||
|
### Conseguenza pratica
|
||||||
|
|
||||||
|
All’istante `t = 0` della prenotazione:
|
||||||
|
|
||||||
|
- se la picking list non è ancora stata lavorata, `XMag_ViewPackingList` e `ViewPackingListRestante` mostrano di fatto lo stesso insieme di UDC
|
||||||
|
- le UDC non scaffalate **restano presenti** in entrambe, perché stanno nella cella convenzionale `1000 = 5E1.1`
|
||||||
|
|
||||||
|
Dopo che il magazziniere ha iniziato i prelievi:
|
||||||
|
|
||||||
|
- le UDC già spedite/prelevate finiscono in `9999 = 7G.1.1`
|
||||||
|
- quindi spariscono da `ViewPackingListRestante`
|
||||||
|
- ma restano ancora visibili in `XMag_ViewPackingList`
|
||||||
|
|
||||||
|
### Limite del comportamento attuale
|
||||||
|
|
||||||
|
Con la summary alta basata su `XMag_ViewPackingList`:
|
||||||
|
|
||||||
|
- il comando `Ricarica` può continuare a mostrare picking list già lavorate in parte
|
||||||
|
- una picking list completamente esaurita può restare visibile nella lista alta
|
||||||
|
- il contenuto della lista alta non coincide più con il “residuo operativo reale”
|
||||||
|
|
||||||
|
In altre parole:
|
||||||
|
|
||||||
|
- la griglia bassa mostra il residuo
|
||||||
|
- la griglia alta mostra ancora lo storico completo
|
||||||
|
|
||||||
|
### Modifica proposta
|
||||||
|
|
||||||
|
La proposta è:
|
||||||
|
|
||||||
|
- usare `ViewPackingListRestante` anche come sorgente della **lista alta aggregata**
|
||||||
|
|
||||||
|
quindi sostituire, nella query summary di [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py):
|
||||||
|
|
||||||
|
- `FROM dbo.XMag_ViewPackingList`
|
||||||
|
|
||||||
|
con:
|
||||||
|
|
||||||
|
- `FROM dbo.ViewPackingListRestante`
|
||||||
|
|
||||||
|
mantenendo invariati:
|
||||||
|
|
||||||
|
- `GROUP BY Documento, CodNazione, NAZIONE, Stato`
|
||||||
|
- `COUNT(DISTINCT Pallet)`
|
||||||
|
- `COUNT(DISTINCT Lotto)`
|
||||||
|
- `COUNT(DISTINCT Articolo)`
|
||||||
|
- `SUM(Qta)`
|
||||||
|
- `MIN(Ordinamento)`
|
||||||
|
- `MAX(IDStato)`
|
||||||
|
|
||||||
|
### Effetti attesi
|
||||||
|
|
||||||
|
Con questa modifica:
|
||||||
|
|
||||||
|
1. all’inizio una picking list nuova continua a comparire normalmente
|
||||||
|
2. durante il lavoro il pulsante `Ricarica` mostra solo ciò che resta davvero da prelevare
|
||||||
|
3. le UDC già finite in `7G.1.1` non inquinano più i conteggi della testata
|
||||||
|
4. una picking list completamente esaurita esce naturalmente dalla lista visibile
|
||||||
|
5. le UDC non scaffalate continuano a comparire correttamente, perché stanno in `1000 = 5E1.1`, non in `9999`
|
||||||
|
|
||||||
|
### Chiusura automatica della prenotazione
|
||||||
|
|
||||||
|
Per mantenere coerenti:
|
||||||
|
|
||||||
|
- griglia alta
|
||||||
|
- stato della prenotazione
|
||||||
|
- comportamento del barcode `F1`
|
||||||
|
|
||||||
|
la prenotazione attiva non deve soltanto sparire dalla griglia: deve essere anche **azzerata logicamente** quando il residuo arriva a zero.
|
||||||
|
|
||||||
|
La patch SQL definitiva quindi deve fare anche questo:
|
||||||
|
|
||||||
|
- dopo ogni movimento, controllare se il documento prenotato ha ancora righe in `ViewPackingListRestante`
|
||||||
|
- se non ne ha più:
|
||||||
|
- la prenotazione attiva va portata a `NULL` / `0`
|
||||||
|
- le eventuali celle residue marcate `IDStato = 1` vanno riportate a `0`
|
||||||
|
|
||||||
|
Effetto operativo:
|
||||||
|
|
||||||
|
- plist piena all’inizio
|
||||||
|
- plist via via ridotta durante i prelievi
|
||||||
|
- plist che, all’ultima UDC, sparisce dalla griglia alta
|
||||||
|
- e nello stesso momento cessa anche di essere la coda `F1`
|
||||||
|
|
||||||
|
### Impatto funzionale
|
||||||
|
|
||||||
|
Questa modifica:
|
||||||
|
|
||||||
|
- **non è** una replica pedissequa del legacy C#
|
||||||
|
- ma è un miglioramento operativo coerente con il comportamento atteso dal magazzino
|
||||||
|
|
||||||
|
In particolare rende la lista alta:
|
||||||
|
|
||||||
|
- più coerente con il significato di “picking list ancora da esaurire”
|
||||||
|
- allineata al contenuto della griglia bassa
|
||||||
|
- più utile nei refresh successivi durante il lavoro reale
|
||||||
|
|
||||||
|
### Impatto tecnico
|
||||||
|
|
||||||
|
Il cambiamento richiesto è piccolo lato Python:
|
||||||
|
|
||||||
|
- aggiornare la query summary in [gestione_pickinglist.py](C:/devel/python/ware_house/gestione_pickinglist.py)
|
||||||
|
|
||||||
|
Non richiede, in prima battuta:
|
||||||
|
|
||||||
|
- modifiche alla stored di prenotazione
|
||||||
|
- modifiche al barcode
|
||||||
|
- modifiche a `ViewPackingListRestante`
|
||||||
|
|
||||||
|
### Decisione progettuale
|
||||||
|
|
||||||
|
Questa modifica va considerata come una scelta esplicita di evoluzione:
|
||||||
|
|
||||||
|
- **se vogliamo restare 1:1 col C#**, la lista alta resta su `XMag_ViewPackingList`
|
||||||
|
- **se vogliamo rendere il modulo più aderente al residuo operativo reale**, conviene passare a `ViewPackingListRestante`
|
||||||
182
apply_online_history_forms_patch.sql
Normal file
182
apply_online_history_forms_patch.sql
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
Patch online - form storiche Python
|
||||||
|
|
||||||
|
Contenuto:
|
||||||
|
- crea/aggiorna le viste Python-only usate dalla form "Storico Picking List"
|
||||||
|
- non modifica stored procedure legacy C#
|
||||||
|
- non modifica le viste legacy C#
|
||||||
|
- non crea oggetti per "Storico movimenti UDC", perche' quella form legge in sola lettura:
|
||||||
|
dbo.MagazziniPallet, dbo.Celle, dbo.XMag_GiacenzaPalletPlistChiuse
|
||||||
|
|
||||||
|
Oggetti creati/aggiornati:
|
||||||
|
- dbo.py_vPreparaPackingListSAMA1
|
||||||
|
- dbo.py_vPreparaPackingList
|
||||||
|
- dbo.py_XMag_ViewPackingListStorico
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[PyPickingListReservation](
|
||||||
|
[ID] [tinyint] NOT NULL,
|
||||||
|
[Documento] [varchar](8) NULL,
|
||||||
|
[IDOperatore] [int] NULL,
|
||||||
|
[ModUtente] [varchar](50) NULL,
|
||||||
|
[ModDataOra] [datetime] NULL,
|
||||||
|
CONSTRAINT [PK_PyPickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||||
|
CONSTRAINT [CK_PyPickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||||
|
) ON [PRIMARY];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingListSAMA1]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
SAMA1.dbo.LOTSER.NUMLOT,
|
||||||
|
SAMA1.dbo.ARTICO.CODICE,
|
||||||
|
SAMA1.dbo.FATRIG.DESCR,
|
||||||
|
LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) AS UDC,
|
||||||
|
SUM(SAMA1.dbo.LOTSER.QTTGIAI) AS Qta,
|
||||||
|
SAMA1.dbo.BAMTES.NUMDOC,
|
||||||
|
SAMA1.dbo.BAMTES.DATDOC,
|
||||||
|
SAMA1.dbo.BAMTES.ID,
|
||||||
|
SAMA1.dbo.BAMTES.DESCRDEST,
|
||||||
|
SAMA1.dbo.BAMTES.STATO AS StatoDocumento,
|
||||||
|
SAMA1.dbo.NAZIONI.CODICE AS Expr1,
|
||||||
|
SAMA1.dbo.MTRASP.CODICE + ' ' + SAMA1.dbo.NAZIONI.DESCR AS NAZIONE,
|
||||||
|
SAMA1.dbo.BAMTES.IDMTRASP
|
||||||
|
FROM SAMA1.dbo.NAZIONI
|
||||||
|
INNER JOIN SAMA1.dbo.BAMTES
|
||||||
|
ON SAMA1.dbo.NAZIONI.ID = SAMA1.dbo.BAMTES.IDNAZDEST
|
||||||
|
RIGHT OUTER JOIN SAMA1.dbo.LOTTIBF
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.LOTSER
|
||||||
|
ON SAMA1.dbo.LOTTIBF.IDLOTSER = SAMA1.dbo.LOTSER.ID
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.ARTICO
|
||||||
|
ON SAMA1.dbo.LOTSER.IDARTICO = SAMA1.dbo.ARTICO.ID
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.FATRIG
|
||||||
|
ON SAMA1.dbo.LOTTIBF.IDFATRIG = SAMA1.dbo.FATRIG.ID
|
||||||
|
ON SAMA1.dbo.BAMTES.ID = SAMA1.dbo.FATRIG.IDBAM
|
||||||
|
LEFT OUTER JOIN SAMA1.sam.EXTUC
|
||||||
|
ON LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) = SAMA1.sam.EXTUC.CODICE
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.MTRASP
|
||||||
|
ON SAMA1.dbo.BAMTES.IDMTRASP = SAMA1.dbo.MTRASP.ID
|
||||||
|
WHERE
|
||||||
|
SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE())
|
||||||
|
AND SAMA1.dbo.BAMTES.STATO IN ('P', 'D')
|
||||||
|
AND SAMA1.dbo.LOTSER.NUMLOT <> '00000000000'
|
||||||
|
GROUP BY
|
||||||
|
SAMA1.dbo.LOTSER.NUMLOT,
|
||||||
|
SAMA1.dbo.ARTICO.CODICE,
|
||||||
|
SAMA1.dbo.FATRIG.DESCR,
|
||||||
|
LEFT(SAMA1.dbo.LOTSER.NUMSER, 6),
|
||||||
|
SAMA1.dbo.BAMTES.NUMDOC,
|
||||||
|
SAMA1.dbo.BAMTES.DATDOC,
|
||||||
|
SAMA1.dbo.BAMTES.ID,
|
||||||
|
SAMA1.dbo.BAMTES.DESCRDEST,
|
||||||
|
SAMA1.dbo.BAMTES.STATO,
|
||||||
|
SAMA1.dbo.NAZIONI.CODICE,
|
||||||
|
SAMA1.dbo.NAZIONI.DESCR,
|
||||||
|
SAMA1.dbo.BAMTES.IDMTRASP,
|
||||||
|
SAMA1.dbo.MTRASP.CODICE;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingList]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
NUMLOT,
|
||||||
|
CODICE,
|
||||||
|
DESCR,
|
||||||
|
UDC,
|
||||||
|
Qta,
|
||||||
|
NUMDOC,
|
||||||
|
DATDOC,
|
||||||
|
ID,
|
||||||
|
DESCRDEST,
|
||||||
|
StatoDocumento,
|
||||||
|
Expr1,
|
||||||
|
NAZIONE
|
||||||
|
FROM dbo.py_vPreparaPackingListSAMA1
|
||||||
|
WHERE NUMLOT <> '';
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingListStorico]
|
||||||
|
AS
|
||||||
|
SELECT TOP (100000)
|
||||||
|
prep.UDC AS Pallet,
|
||||||
|
prep.NUMLOT AS Lotto,
|
||||||
|
prep.CODICE AS Articolo,
|
||||||
|
prep.DESCR AS Descrizione,
|
||||||
|
prep.Qta,
|
||||||
|
prep.NUMDOC AS Documento,
|
||||||
|
prep.DATDOC AS DataDocumento,
|
||||||
|
prep.StatoDocumento,
|
||||||
|
prep.Expr1 AS CodNazione,
|
||||||
|
prep.NAZIONE,
|
||||||
|
CASE
|
||||||
|
WHEN prep.Expr1 = 'DE' THEN 10
|
||||||
|
WHEN prep.Expr1 = 'TH' THEN
|
||||||
|
CASE WHEN SUBSTRING(prep.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END
|
||||||
|
WHEN prep.Expr1 = 'MEX' THEN 12
|
||||||
|
ELSE 4
|
||||||
|
END AS Stato,
|
||||||
|
ISNULL(g.NumeroPallet, 0) AS PalletCella,
|
||||||
|
ISNULL(g.IDMagazzino, 1) AS Magazzino,
|
||||||
|
ISNULL(g.IDArea, 5) AS Area,
|
||||||
|
ISNULL(g.IDCella, 1000) AS Cella,
|
||||||
|
ISNULL(c.Ordinamento, 99999) AS Ordinamento,
|
||||||
|
ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione,
|
||||||
|
SUBSTRING(prep.DESCRDEST, 1, 2) AS DEST,
|
||||||
|
CASE
|
||||||
|
WHEN MAX(CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(prep.NUMDOC AS varchar(8))
|
||||||
|
THEN 1 ELSE 0 END) = 1
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS IDStato
|
||||||
|
FROM dbo.Celle c
|
||||||
|
INNER JOIN dbo.XMag_GiacenzaPallet g
|
||||||
|
ON c.ID = g.IDCella
|
||||||
|
RIGHT OUTER JOIN dbo.py_vPreparaPackingList prep
|
||||||
|
ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC
|
||||||
|
LEFT JOIN dbo.PyPickingListReservation pr
|
||||||
|
ON pr.ID = 1
|
||||||
|
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
prep.Expr1,
|
||||||
|
prep.NAZIONE,
|
||||||
|
prep.UDC,
|
||||||
|
prep.NUMDOC,
|
||||||
|
prep.DATDOC,
|
||||||
|
prep.StatoDocumento,
|
||||||
|
prep.NUMLOT,
|
||||||
|
prep.CODICE,
|
||||||
|
prep.DESCR,
|
||||||
|
prep.Qta,
|
||||||
|
g.NumeroPallet,
|
||||||
|
g.IDMagazzino,
|
||||||
|
g.IDArea,
|
||||||
|
g.IDCella,
|
||||||
|
c.Ordinamento,
|
||||||
|
c.Corsia,
|
||||||
|
c.Colonna,
|
||||||
|
c.Fila,
|
||||||
|
prep.DESCRDEST;
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT 'Patch form storiche Python applicata.';
|
||||||
|
PRINT 'Creato/aggiornato dbo.py_vPreparaPackingListSAMA1';
|
||||||
|
PRINT 'Creato/aggiornato dbo.py_vPreparaPackingList';
|
||||||
|
PRINT 'Creato/aggiornato dbo.py_XMag_ViewPackingListStorico';
|
||||||
|
GO
|
||||||
408
apply_plist_reservation_patch.sql
Normal file
408
apply_plist_reservation_patch.sql
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/*
|
||||||
|
Patch deploy per i test sul campo:
|
||||||
|
- salva le definizioni correnti degli oggetti legacy coinvolti
|
||||||
|
- applica la nuova logica di prenotazione plist a livello documento
|
||||||
|
|
||||||
|
Oggetti salvati:
|
||||||
|
- dbo.sp_xExePackingListPallet
|
||||||
|
- dbo.XMag_ViewPackingList
|
||||||
|
- dbo.sp_xExePackingListPalletPrenota
|
||||||
|
- dbo.sp_ControllaPrenotazionePackingListPalletNew
|
||||||
|
|
||||||
|
Oggetti creati/usati dalla patch:
|
||||||
|
- dbo.WarehouseObjectBackup
|
||||||
|
- dbo.PickingListReservation
|
||||||
|
|
||||||
|
Nota:
|
||||||
|
- la tabella dbo.PickingListReservation può restare anche dopo il rollback
|
||||||
|
perché il vecchio codice non la usa.
|
||||||
|
*/
|
||||||
|
|
||||||
|
DECLARE @BackupTag varchar(64) = 'plist_reservation_fix_alpha2';
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.WarehouseObjectBackup', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.WarehouseObjectBackup (
|
||||||
|
BackupTag varchar(64) NOT NULL,
|
||||||
|
ObjectName sysname NOT NULL,
|
||||||
|
ObjectType varchar(16) NOT NULL,
|
||||||
|
Definition nvarchar(max) NOT NULL,
|
||||||
|
SavedAt datetime NOT NULL CONSTRAINT DF_WarehouseObjectBackup_SavedAt DEFAULT(GETDATE()),
|
||||||
|
CONSTRAINT PK_WarehouseObjectBackup PRIMARY KEY CLUSTERED (BackupTag, ObjectName)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
DECLARE @BackupTag varchar(64) = 'plist_reservation_fix_alpha2';
|
||||||
|
|
||||||
|
;WITH Targets AS (
|
||||||
|
SELECT N'dbo.sp_xExePackingListPallet' AS ObjectName, 'PROCEDURE' AS ObjectType
|
||||||
|
UNION ALL SELECT N'dbo.XMag_ViewPackingList', 'VIEW'
|
||||||
|
UNION ALL SELECT N'dbo.sp_xExePackingListPalletPrenota', 'PROCEDURE'
|
||||||
|
UNION ALL SELECT N'dbo.sp_ControllaPrenotazionePackingListPalletNew', 'PROCEDURE'
|
||||||
|
)
|
||||||
|
INSERT INTO dbo.WarehouseObjectBackup (BackupTag, ObjectName, ObjectType, Definition)
|
||||||
|
SELECT
|
||||||
|
@BackupTag,
|
||||||
|
t.ObjectName,
|
||||||
|
t.ObjectType,
|
||||||
|
sm.[definition]
|
||||||
|
FROM Targets t
|
||||||
|
INNER JOIN sys.sql_modules sm
|
||||||
|
ON sm.object_id = OBJECT_ID(t.ObjectName)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.WarehouseObjectBackup b
|
||||||
|
WHERE b.BackupTag = @BackupTag
|
||||||
|
AND b.ObjectName = t.ObjectName
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Patch prenotazione Picking List a livello documento
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
- rendere esclusiva la prenotazione di una sola picking list
|
||||||
|
- mantenere la stored con lo stesso nome:
|
||||||
|
dbo.sp_xExePackingListPallet
|
||||||
|
- introdurre una sorgente di verità per documento prenotato:
|
||||||
|
dbo.PickingListReservation
|
||||||
|
- far leggere XMag_ViewPackingList.IDStato da tale stato
|
||||||
|
e non più soltanto da Celle.IDStato
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.PickingListReservation', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[PickingListReservation](
|
||||||
|
[ID] [tinyint] NOT NULL,
|
||||||
|
[Documento] [varchar](8) NULL,
|
||||||
|
[IDOperatore] [int] NULL,
|
||||||
|
[ModUtente] [varchar](50) NULL,
|
||||||
|
[ModDataOra] [datetime] NULL,
|
||||||
|
CONSTRAINT [PK_PickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||||
|
CONSTRAINT [CK_PickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||||
|
) ON [PRIMARY];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPallet]
|
||||||
|
@IDOperatore int,
|
||||||
|
@Documento varchar(8),
|
||||||
|
@Azione char(1) = 'P',
|
||||||
|
@RC int OUTPUT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET @RC = 0;
|
||||||
|
|
||||||
|
DECLARE @Nominativo varchar(50) = '';
|
||||||
|
DECLARE @DocumentoAttivo varchar(8) = NULL;
|
||||||
|
DECLARE @Description varchar(255) = '';
|
||||||
|
DECLARE @Message varchar(255) = '';
|
||||||
|
DECLARE @IDResult int = 0;
|
||||||
|
DECLARE @ID int = 0;
|
||||||
|
|
||||||
|
SELECT @Nominativo = [Login]
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
IF @Azione NOT IN ('P', 'S')
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -10;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -20;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT @DocumentoAttivo = NULLIF(LTRIM(RTRIM(Documento)), '')
|
||||||
|
FROM dbo.PickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
DECLARE @TargetCelle TABLE (
|
||||||
|
IDCella int PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @TargetCelle (IDCella)
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL;
|
||||||
|
|
||||||
|
IF @Azione = 'P'
|
||||||
|
BEGIN
|
||||||
|
IF @DocumentoAttivo = @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 0,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE ISNULL(c.IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = @Documento,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
SELECT TOP 1 @Description = NAZIONE
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento;
|
||||||
|
|
||||||
|
EXEC dbo.sp_LogPackingList
|
||||||
|
@ID = @ID,
|
||||||
|
@Code = @Documento,
|
||||||
|
@Description = @Description,
|
||||||
|
@Message = @Message OUTPUT,
|
||||||
|
@IDResult = @IDResult OUTPUT;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @Azione = 'S'
|
||||||
|
BEGIN
|
||||||
|
IF ISNULL(@DocumentoAttivo, '') <> @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[XMag_ViewPackingList]
|
||||||
|
AS
|
||||||
|
WITH Base AS (
|
||||||
|
SELECT
|
||||||
|
dbo.vPreparaPackingList.UDC AS Pallet,
|
||||||
|
dbo.vPreparaPackingList.NUMLOT AS Lotto,
|
||||||
|
dbo.vPreparaPackingList.CODICE AS Articolo,
|
||||||
|
dbo.vPreparaPackingList.DESCR AS Descrizione,
|
||||||
|
dbo.vPreparaPackingList.Qta,
|
||||||
|
dbo.vPreparaPackingList.NUMDOC AS Documento,
|
||||||
|
dbo.vPreparaPackingList.Expr1 AS CodNazione,
|
||||||
|
dbo.vPreparaPackingList.NAZIONE,
|
||||||
|
CASE
|
||||||
|
WHEN Expr1 = 'DE' THEN 10
|
||||||
|
WHEN Expr1 = 'TH' THEN CASE WHEN SUBSTRING(dbo.vPreparaPackingList.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END
|
||||||
|
WHEN Expr1 = 'MEX' THEN 12
|
||||||
|
ELSE 4
|
||||||
|
END AS Stato,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.NumeroPallet, 0) AS PalletCella,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDMagazzino, 1) AS Magazzino,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDArea, 5) AS Area,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDCella, 1000) AS Cella,
|
||||||
|
ISNULL(dbo.Celle.Ordinamento, 99999) AS Ordinamento,
|
||||||
|
ISNULL(dbo.Celle.Corsia + ' - ' + dbo.Celle.Colonna + ' - ' + dbo.Celle.Fila, 'Non scaff.') AS Ubicazione,
|
||||||
|
SUBSTRING(dbo.vPreparaPackingList.DESCRDEST, 1, 2) AS DEST
|
||||||
|
FROM dbo.Celle
|
||||||
|
INNER JOIN dbo.XMag_GiacenzaPallet
|
||||||
|
ON dbo.Celle.ID = dbo.XMag_GiacenzaPallet.IDCella
|
||||||
|
RIGHT OUTER JOIN dbo.vPreparaPackingList
|
||||||
|
ON dbo.XMag_GiacenzaPallet.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = dbo.vPreparaPackingList.UDC
|
||||||
|
GROUP BY
|
||||||
|
dbo.vPreparaPackingList.Expr1,
|
||||||
|
dbo.vPreparaPackingList.NAZIONE,
|
||||||
|
dbo.vPreparaPackingList.UDC,
|
||||||
|
dbo.vPreparaPackingList.NUMDOC,
|
||||||
|
dbo.vPreparaPackingList.NUMLOT,
|
||||||
|
dbo.vPreparaPackingList.CODICE,
|
||||||
|
dbo.vPreparaPackingList.DESCR,
|
||||||
|
dbo.vPreparaPackingList.Qta,
|
||||||
|
dbo.XMag_GiacenzaPallet.NumeroPallet,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDMagazzino,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDArea,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDCella,
|
||||||
|
dbo.Celle.Ordinamento,
|
||||||
|
dbo.Celle.Corsia,
|
||||||
|
dbo.Celle.Colonna,
|
||||||
|
dbo.Celle.Fila,
|
||||||
|
dbo.vPreparaPackingList.DESCRDEST
|
||||||
|
)
|
||||||
|
SELECT TOP 10000
|
||||||
|
Base.Pallet,
|
||||||
|
Base.Lotto,
|
||||||
|
Base.Articolo,
|
||||||
|
Base.Descrizione,
|
||||||
|
Base.Qta,
|
||||||
|
Base.Documento,
|
||||||
|
Base.CodNazione,
|
||||||
|
Base.NAZIONE,
|
||||||
|
Base.Stato,
|
||||||
|
Base.PalletCella,
|
||||||
|
Base.Magazzino,
|
||||||
|
Base.Area,
|
||||||
|
Base.Cella,
|
||||||
|
Base.Ordinamento,
|
||||||
|
Base.Ubicazione,
|
||||||
|
Base.DEST,
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(Base.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS IDStato
|
||||||
|
FROM Base
|
||||||
|
LEFT JOIN dbo.PickingListReservation pr
|
||||||
|
ON pr.ID = 1
|
||||||
|
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(Base.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END DESC,
|
||||||
|
Base.Documento,
|
||||||
|
Base.Ordinamento;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPalletPrenota]
|
||||||
|
@IDOperatore int,
|
||||||
|
@Documento varchar(8),
|
||||||
|
@RC int OUTPUT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET @RC = 0;
|
||||||
|
|
||||||
|
DECLARE @Nominativo varchar(50) = '';
|
||||||
|
|
||||||
|
SELECT @Nominativo = LOGIN
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 1,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE c.ID IN (
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[sp_ControllaPrenotazionePackingListPalletNew]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Documento varchar(8) = NULL;
|
||||||
|
DECLARE @IDOperatore int = 0;
|
||||||
|
DECLARE @Nominativo varchar(50) = 'SYSTEM';
|
||||||
|
DECLARE @RC int = 0;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
@Documento = NULLIF(LTRIM(RTRIM(Documento)), ''),
|
||||||
|
@IDOperatore = ISNULL(IDOperatore, 0),
|
||||||
|
@Nominativo = ISNULL(NULLIF(LTRIM(RTRIM(ModUtente)), ''), 'SYSTEM')
|
||||||
|
FROM dbo.PickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
IF ISNULL(@Documento, '') = ''
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
IF @IDOperatore <= 0
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP 1 @IDOperatore = ID
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE LOGIN = @Nominativo;
|
||||||
|
END;
|
||||||
|
|
||||||
|
EXEC dbo.sp_xExePackingListPalletPrenota
|
||||||
|
@IDOperatore = @IDOperatore,
|
||||||
|
@Documento = @Documento,
|
||||||
|
@RC = @RC OUTPUT;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
328
apply_python_parallel_pickinglist_patch.sql
Normal file
328
apply_python_parallel_pickinglist_patch.sql
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/*
|
||||||
|
Patch parallela per usare C# e Python contemporaneamente.
|
||||||
|
|
||||||
|
Questa patch NON modifica gli oggetti legacy usati dal C#.
|
||||||
|
|
||||||
|
Il C# continua a usare:
|
||||||
|
- dbo.XMag_ViewPackingList
|
||||||
|
- dbo.ViewPackingListRestante
|
||||||
|
- dbo.sp_xExePackingListPallet
|
||||||
|
- dbo.sp_xExePackingListPalletPrenota
|
||||||
|
- dbo.sp_ControllaPrenotazionePackingListPalletNew
|
||||||
|
|
||||||
|
Il Python usa invece:
|
||||||
|
- dbo.py_XMag_ViewPackingList
|
||||||
|
- dbo.py_ViewPackingListRestante
|
||||||
|
- dbo.py_sp_xExePackingListPallet
|
||||||
|
- dbo.py_sp_xExePackingListPalletPrenota
|
||||||
|
- dbo.py_sp_ControllaPrenotazionePackingListPalletNew
|
||||||
|
- dbo.PyPickingListReservation
|
||||||
|
*/
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[PyPickingListReservation](
|
||||||
|
[ID] [tinyint] NOT NULL,
|
||||||
|
[Documento] [varchar](8) NULL,
|
||||||
|
[IDOperatore] [int] NULL,
|
||||||
|
[ModUtente] [varchar](50) NULL,
|
||||||
|
[ModDataOra] [datetime] NULL,
|
||||||
|
CONSTRAINT [PK_PyPickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||||
|
CONSTRAINT [CK_PyPickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||||
|
) ON [PRIMARY];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingList]
|
||||||
|
AS
|
||||||
|
SELECT TOP 10000
|
||||||
|
legacy.Pallet,
|
||||||
|
legacy.Lotto,
|
||||||
|
legacy.Articolo,
|
||||||
|
legacy.Descrizione,
|
||||||
|
legacy.Qta,
|
||||||
|
legacy.Documento,
|
||||||
|
legacy.CodNazione,
|
||||||
|
legacy.NAZIONE,
|
||||||
|
legacy.Stato,
|
||||||
|
legacy.PalletCella,
|
||||||
|
legacy.Magazzino,
|
||||||
|
legacy.Area,
|
||||||
|
legacy.Cella,
|
||||||
|
legacy.Ordinamento,
|
||||||
|
legacy.Ubicazione,
|
||||||
|
legacy.DEST,
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(legacy.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS IDStato
|
||||||
|
FROM dbo.XMag_ViewPackingList legacy
|
||||||
|
LEFT JOIN dbo.PyPickingListReservation pr
|
||||||
|
ON pr.ID = 1
|
||||||
|
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(legacy.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END DESC,
|
||||||
|
legacy.Documento,
|
||||||
|
legacy.Ordinamento;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_ViewPackingListRestante]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
Pallet,
|
||||||
|
Lotto,
|
||||||
|
Articolo,
|
||||||
|
Descrizione,
|
||||||
|
Qta,
|
||||||
|
Documento,
|
||||||
|
CodNazione,
|
||||||
|
NAZIONE,
|
||||||
|
Stato,
|
||||||
|
PalletCella,
|
||||||
|
Magazzino,
|
||||||
|
Area,
|
||||||
|
Cella,
|
||||||
|
Ordinamento,
|
||||||
|
Ubicazione,
|
||||||
|
DEST,
|
||||||
|
IDStato
|
||||||
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
|
WHERE Cella <> 9999;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[py_sp_xExePackingListPallet]
|
||||||
|
@IDOperatore int,
|
||||||
|
@Documento varchar(8),
|
||||||
|
@Azione char(1) = 'P',
|
||||||
|
@RC int OUTPUT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET @RC = 0;
|
||||||
|
|
||||||
|
DECLARE @Nominativo varchar(50) = '';
|
||||||
|
DECLARE @DocumentoAttivo varchar(8) = NULL;
|
||||||
|
DECLARE @Description varchar(255) = '';
|
||||||
|
DECLARE @Message varchar(255) = '';
|
||||||
|
DECLARE @IDResult int = 0;
|
||||||
|
DECLARE @ID int = 0;
|
||||||
|
|
||||||
|
SELECT @Nominativo = [Login]
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
IF @Azione NOT IN ('P', 'S')
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -10;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -20;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PyPickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PyPickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT @DocumentoAttivo = NULLIF(LTRIM(RTRIM(Documento)), '')
|
||||||
|
FROM dbo.PyPickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
DECLARE @TargetCelle TABLE (
|
||||||
|
IDCella int PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @TargetCelle (IDCella)
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL;
|
||||||
|
|
||||||
|
IF @Azione = 'P'
|
||||||
|
BEGIN
|
||||||
|
IF @DocumentoAttivo = @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 0,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE ISNULL(c.IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PyPickingListReservation
|
||||||
|
SET Documento = @Documento,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
SELECT TOP 1 @Description = NAZIONE
|
||||||
|
FROM dbo.py_XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento;
|
||||||
|
|
||||||
|
EXEC dbo.sp_LogPackingList
|
||||||
|
@ID = @ID,
|
||||||
|
@Code = @Documento,
|
||||||
|
@Description = @Description,
|
||||||
|
@Message = @Message OUTPUT,
|
||||||
|
@IDResult = @IDResult OUTPUT;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @Azione = 'S'
|
||||||
|
BEGIN
|
||||||
|
IF ISNULL(@DocumentoAttivo, '') <> @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PyPickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[py_sp_xExePackingListPalletPrenota]
|
||||||
|
@IDOperatore int,
|
||||||
|
@Documento varchar(8),
|
||||||
|
@RC int OUTPUT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET @RC = 0;
|
||||||
|
|
||||||
|
DECLARE @Nominativo varchar(50) = '';
|
||||||
|
|
||||||
|
SELECT @Nominativo = LOGIN
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 1,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE c.ID IN (
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.py_ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[py_sp_ControllaPrenotazionePackingListPalletNew]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Documento varchar(8) = NULL;
|
||||||
|
DECLARE @IDOperatore int = 0;
|
||||||
|
DECLARE @Nominativo varchar(50) = 'SYSTEM';
|
||||||
|
DECLARE @RC int = 0;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
@Documento = NULLIF(LTRIM(RTRIM(Documento)), ''),
|
||||||
|
@IDOperatore = ISNULL(IDOperatore, 0),
|
||||||
|
@Nominativo = ISNULL(NULLIF(LTRIM(RTRIM(ModUtente)), ''), 'SYSTEM')
|
||||||
|
FROM dbo.PyPickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
IF ISNULL(@Documento, '') = ''
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.py_ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
UPDATE dbo.PyPickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
IF @IDOperatore <= 0
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP 1 @IDOperatore = ID
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE LOGIN = @Nominativo;
|
||||||
|
END;
|
||||||
|
|
||||||
|
EXEC dbo.py_sp_xExePackingListPalletPrenota
|
||||||
|
@IDOperatore = @IDOperatore,
|
||||||
|
@Documento = @Documento,
|
||||||
|
@RC = @RC OUTPUT;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
158
apply_python_pickinglist_history_views.sql
Normal file
158
apply_python_pickinglist_history_views.sql
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
Patch SQL - viste storiche picking list per ramo Python
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
- creare un ramo di lettura storico completamente separato dalle viste C#
|
||||||
|
- non modificare dbo.vPreparaPackingListSAMA1
|
||||||
|
- non modificare dbo.vPreparaPackingList
|
||||||
|
- non modificare dbo.XMag_ViewPackingList
|
||||||
|
- non modificare stored procedure C#
|
||||||
|
|
||||||
|
La form Python "Storico Picking List" legge da dbo.py_XMag_ViewPackingListStorico.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingListSAMA1]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
SAMA1.dbo.LOTSER.NUMLOT,
|
||||||
|
SAMA1.dbo.ARTICO.CODICE,
|
||||||
|
SAMA1.dbo.FATRIG.DESCR,
|
||||||
|
LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) AS UDC,
|
||||||
|
SUM(SAMA1.dbo.LOTSER.QTTGIAI) AS Qta,
|
||||||
|
SAMA1.dbo.BAMTES.NUMDOC,
|
||||||
|
SAMA1.dbo.BAMTES.DATDOC,
|
||||||
|
SAMA1.dbo.BAMTES.ID,
|
||||||
|
SAMA1.dbo.BAMTES.DESCRDEST,
|
||||||
|
SAMA1.dbo.BAMTES.STATO AS StatoDocumento,
|
||||||
|
SAMA1.dbo.NAZIONI.CODICE AS Expr1,
|
||||||
|
SAMA1.dbo.MTRASP.CODICE + ' ' + SAMA1.dbo.NAZIONI.DESCR AS NAZIONE,
|
||||||
|
SAMA1.dbo.BAMTES.IDMTRASP
|
||||||
|
FROM SAMA1.dbo.NAZIONI
|
||||||
|
INNER JOIN SAMA1.dbo.BAMTES
|
||||||
|
ON SAMA1.dbo.NAZIONI.ID = SAMA1.dbo.BAMTES.IDNAZDEST
|
||||||
|
RIGHT OUTER JOIN SAMA1.dbo.LOTTIBF
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.LOTSER
|
||||||
|
ON SAMA1.dbo.LOTTIBF.IDLOTSER = SAMA1.dbo.LOTSER.ID
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.ARTICO
|
||||||
|
ON SAMA1.dbo.LOTSER.IDARTICO = SAMA1.dbo.ARTICO.ID
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.FATRIG
|
||||||
|
ON SAMA1.dbo.LOTTIBF.IDFATRIG = SAMA1.dbo.FATRIG.ID
|
||||||
|
ON SAMA1.dbo.BAMTES.ID = SAMA1.dbo.FATRIG.IDBAM
|
||||||
|
LEFT OUTER JOIN SAMA1.sam.EXTUC
|
||||||
|
ON LEFT(SAMA1.dbo.LOTSER.NUMSER, 6) = SAMA1.sam.EXTUC.CODICE
|
||||||
|
LEFT OUTER JOIN SAMA1.dbo.MTRASP
|
||||||
|
ON SAMA1.dbo.BAMTES.IDMTRASP = SAMA1.dbo.MTRASP.ID
|
||||||
|
WHERE
|
||||||
|
SAMA1.dbo.BAMTES.ANNDOC >= YEAR(GETDATE())
|
||||||
|
AND SAMA1.dbo.BAMTES.STATO IN ('P', 'D')
|
||||||
|
AND SAMA1.dbo.LOTSER.NUMLOT <> '00000000000'
|
||||||
|
GROUP BY
|
||||||
|
SAMA1.dbo.LOTSER.NUMLOT,
|
||||||
|
SAMA1.dbo.ARTICO.CODICE,
|
||||||
|
SAMA1.dbo.FATRIG.DESCR,
|
||||||
|
LEFT(SAMA1.dbo.LOTSER.NUMSER, 6),
|
||||||
|
SAMA1.dbo.BAMTES.NUMDOC,
|
||||||
|
SAMA1.dbo.BAMTES.DATDOC,
|
||||||
|
SAMA1.dbo.BAMTES.ID,
|
||||||
|
SAMA1.dbo.BAMTES.DESCRDEST,
|
||||||
|
SAMA1.dbo.BAMTES.STATO,
|
||||||
|
SAMA1.dbo.NAZIONI.CODICE,
|
||||||
|
SAMA1.dbo.NAZIONI.DESCR,
|
||||||
|
SAMA1.dbo.BAMTES.IDMTRASP,
|
||||||
|
SAMA1.dbo.MTRASP.CODICE;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_vPreparaPackingList]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
NUMLOT,
|
||||||
|
CODICE,
|
||||||
|
DESCR,
|
||||||
|
UDC,
|
||||||
|
Qta,
|
||||||
|
NUMDOC,
|
||||||
|
DATDOC,
|
||||||
|
ID,
|
||||||
|
DESCRDEST,
|
||||||
|
StatoDocumento,
|
||||||
|
Expr1,
|
||||||
|
NAZIONE
|
||||||
|
FROM dbo.py_vPreparaPackingListSAMA1
|
||||||
|
WHERE NUMLOT <> '';
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[py_XMag_ViewPackingListStorico]
|
||||||
|
AS
|
||||||
|
SELECT TOP (100000)
|
||||||
|
prep.UDC AS Pallet,
|
||||||
|
prep.NUMLOT AS Lotto,
|
||||||
|
prep.CODICE AS Articolo,
|
||||||
|
prep.DESCR AS Descrizione,
|
||||||
|
prep.Qta,
|
||||||
|
prep.NUMDOC AS Documento,
|
||||||
|
prep.DATDOC AS DataDocumento,
|
||||||
|
prep.StatoDocumento,
|
||||||
|
prep.Expr1 AS CodNazione,
|
||||||
|
prep.NAZIONE,
|
||||||
|
CASE
|
||||||
|
WHEN prep.Expr1 = 'DE' THEN 10
|
||||||
|
WHEN prep.Expr1 = 'TH' THEN
|
||||||
|
CASE WHEN SUBSTRING(prep.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END
|
||||||
|
WHEN prep.Expr1 = 'MEX' THEN 12
|
||||||
|
ELSE 4
|
||||||
|
END AS Stato,
|
||||||
|
ISNULL(g.NumeroPallet, 0) AS PalletCella,
|
||||||
|
ISNULL(g.IDMagazzino, 1) AS Magazzino,
|
||||||
|
ISNULL(g.IDArea, 5) AS Area,
|
||||||
|
ISNULL(g.IDCella, 1000) AS Cella,
|
||||||
|
ISNULL(c.Ordinamento, 99999) AS Ordinamento,
|
||||||
|
ISNULL(c.Corsia + ' - ' + c.Colonna + ' - ' + c.Fila, 'Non scaff.') AS Ubicazione,
|
||||||
|
SUBSTRING(prep.DESCRDEST, 1, 2) AS DEST,
|
||||||
|
CASE
|
||||||
|
WHEN MAX(CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(prep.NUMDOC AS varchar(8))
|
||||||
|
THEN 1 ELSE 0 END) = 1
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS IDStato
|
||||||
|
FROM dbo.Celle c
|
||||||
|
INNER JOIN dbo.XMag_GiacenzaPallet g
|
||||||
|
ON c.ID = g.IDCella
|
||||||
|
RIGHT OUTER JOIN dbo.py_vPreparaPackingList prep
|
||||||
|
ON g.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = prep.UDC
|
||||||
|
LEFT JOIN dbo.PyPickingListReservation pr
|
||||||
|
ON pr.ID = 1
|
||||||
|
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
prep.Expr1,
|
||||||
|
prep.NAZIONE,
|
||||||
|
prep.UDC,
|
||||||
|
prep.NUMDOC,
|
||||||
|
prep.DATDOC,
|
||||||
|
prep.StatoDocumento,
|
||||||
|
prep.NUMLOT,
|
||||||
|
prep.CODICE,
|
||||||
|
prep.DESCR,
|
||||||
|
prep.Qta,
|
||||||
|
g.NumeroPallet,
|
||||||
|
g.IDMagazzino,
|
||||||
|
g.IDArea,
|
||||||
|
g.IDCella,
|
||||||
|
c.Ordinamento,
|
||||||
|
c.Corsia,
|
||||||
|
c.Colonna,
|
||||||
|
c.Fila,
|
||||||
|
prep.DESCRDEST;
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT 'Viste storiche Python create/aggiornate:';
|
||||||
|
PRINT ' - dbo.py_vPreparaPackingListSAMA1';
|
||||||
|
PRINT ' - dbo.py_vPreparaPackingList';
|
||||||
|
PRINT ' - dbo.py_XMag_ViewPackingListStorico';
|
||||||
|
GO
|
||||||
@@ -7,6 +7,7 @@ singleton so every module can schedule work on the same async runtime.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
class _LoopHolder:
|
class _LoopHolder:
|
||||||
@@ -15,6 +16,7 @@ class _LoopHolder:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
self.closing = False
|
||||||
|
|
||||||
|
|
||||||
_GLOBAL = _LoopHolder()
|
_GLOBAL = _LoopHolder()
|
||||||
@@ -26,7 +28,7 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
|||||||
The loop is created lazily the first time the function is called and kept
|
The loop is created lazily the first time the function is called and kept
|
||||||
alive for the lifetime of the application.
|
alive for the lifetime of the application.
|
||||||
"""
|
"""
|
||||||
if _GLOBAL.loop:
|
if _GLOBAL.loop and not _GLOBAL.closing:
|
||||||
return _GLOBAL.loop
|
return _GLOBAL.loop
|
||||||
|
|
||||||
ready = threading.Event()
|
ready = threading.Event()
|
||||||
@@ -35,7 +37,23 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
|||||||
_GLOBAL.loop = asyncio.new_event_loop()
|
_GLOBAL.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(_GLOBAL.loop)
|
asyncio.set_event_loop(_GLOBAL.loop)
|
||||||
ready.set()
|
ready.set()
|
||||||
|
try:
|
||||||
_GLOBAL.loop.run_forever()
|
_GLOBAL.loop.run_forever()
|
||||||
|
finally:
|
||||||
|
loop = _GLOBAL.loop
|
||||||
|
if loop is not None:
|
||||||
|
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
if pending:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
loop.run_until_complete(loop.shutdown_default_executor())
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
loop.close()
|
||||||
|
|
||||||
_GLOBAL.thread = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
|
_GLOBAL.thread = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
|
||||||
_GLOBAL.thread.start()
|
_GLOBAL.thread.start()
|
||||||
@@ -46,7 +64,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
|||||||
def stop_global_loop():
|
def stop_global_loop():
|
||||||
"""Stop the shared loop and join the background thread if present."""
|
"""Stop the shared loop and join the background thread if present."""
|
||||||
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
||||||
|
_GLOBAL.closing = True
|
||||||
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
||||||
_GLOBAL.thread.join(timeout=2)
|
_GLOBAL.thread.join(timeout=2)
|
||||||
_GLOBAL.loop = None
|
_GLOBAL.loop = None
|
||||||
_GLOBAL.thread = None
|
_GLOBAL.thread = None
|
||||||
|
_GLOBAL.closing = False
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ from sqlalchemy import text
|
|||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
|
||||||
|
# The desktop app opens short-lived SQL connections from a background
|
||||||
|
# asyncio loop. ODBC pooling can keep native handles alive for a while after
|
||||||
|
# the GUI closes, which is especially visible with pythonw.
|
||||||
|
pyodbc.pooling = False
|
||||||
|
except Exception:
|
||||||
|
pyodbc = None # type: ignore[assignment]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import orjson as _json
|
import orjson as _json
|
||||||
|
|
||||||
@@ -120,6 +130,7 @@ class AsyncMSSQLClient:
|
|||||||
params: Optional[Dict[str, Any]] = None,
|
params: Optional[Dict[str, Any]] = None,
|
||||||
*,
|
*,
|
||||||
as_dict_rows: bool = False,
|
as_dict_rows: bool = False,
|
||||||
|
commit: bool = False,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Execute a query and return a JSON-friendly payload.
|
"""Execute a query and return a JSON-friendly payload.
|
||||||
|
|
||||||
@@ -128,6 +139,9 @@ class AsyncMSSQLClient:
|
|||||||
params: Optional named parameters bound to the statement.
|
params: Optional named parameters bound to the statement.
|
||||||
as_dict_rows: When ``True`` returns rows as dictionaries keyed by
|
as_dict_rows: When ``True`` returns rows as dictionaries keyed by
|
||||||
column name; otherwise rows are returned as lists.
|
column name; otherwise rows are returned as lists.
|
||||||
|
commit: When ``True`` the statement runs in a transaction that is
|
||||||
|
committed on success. Useful for SQL batches that both mutate
|
||||||
|
data and return a final result set.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary containing column names, rows and elapsed execution
|
A dictionary containing column names, rows and elapsed execution
|
||||||
@@ -135,7 +149,7 @@ class AsyncMSSQLClient:
|
|||||||
"""
|
"""
|
||||||
await self._ensure_engine()
|
await self._ensure_engine()
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
async with self._engine.connect() as conn:
|
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
|
||||||
res = await conn.execute(text(sql), params or {})
|
res = await conn.execute(text(sql), params or {})
|
||||||
rows = res.fetchall()
|
rows = res.fetchall()
|
||||||
cols = list(res.keys())
|
cols = list(res.keys())
|
||||||
|
|||||||
102
audit_log.py
Normal file
102
audit_log.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""Central textual audit log for user-driven warehouse operations."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from user_session import UserSession
|
||||||
|
|
||||||
|
|
||||||
|
AUDIT_LOG_PATH = Path(__file__).with_name("warehouse_audit.log")
|
||||||
|
_LOGGER = logging.getLogger("warehouse_audit")
|
||||||
|
_LOGGER_CONFIGURED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_logger() -> None:
|
||||||
|
"""Configure the append-only textual audit logger once."""
|
||||||
|
|
||||||
|
global _LOGGER_CONFIGURED
|
||||||
|
if _LOGGER_CONFIGURED:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.setLevel(logging.INFO)
|
||||||
|
_LOGGER.propagate = False
|
||||||
|
handler = logging.FileHandler(AUDIT_LOG_PATH, encoding="utf-8")
|
||||||
|
handler.setFormatter(logging.Formatter("%(asctime)s | %(message)s"))
|
||||||
|
_LOGGER.addHandler(handler)
|
||||||
|
_LOGGER_CONFIGURED = True
|
||||||
|
|
||||||
|
|
||||||
|
def _user_label(session: UserSession | None) -> str:
|
||||||
|
"""Render the user identity consistently in the audit trail."""
|
||||||
|
|
||||||
|
if session is None:
|
||||||
|
return "anonymous"
|
||||||
|
return f"{session.login or 'anonymous'}#{session.operator_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _payload(
|
||||||
|
*,
|
||||||
|
session: UserSession | None,
|
||||||
|
module: str,
|
||||||
|
action: str,
|
||||||
|
outcome: str,
|
||||||
|
target: str = "",
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Serialize one audit event as a compact text line."""
|
||||||
|
|
||||||
|
base = {
|
||||||
|
"user": _user_label(session),
|
||||||
|
"module": module,
|
||||||
|
"action": action,
|
||||||
|
"outcome": outcome,
|
||||||
|
"target": target,
|
||||||
|
"details": details or {},
|
||||||
|
}
|
||||||
|
return json.dumps(base, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
def log_user_action(
|
||||||
|
session: UserSession | None,
|
||||||
|
*,
|
||||||
|
module: str,
|
||||||
|
action: str,
|
||||||
|
outcome: str,
|
||||||
|
target: str = "",
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Write one user action event to the textual audit file."""
|
||||||
|
|
||||||
|
_configure_logger()
|
||||||
|
_LOGGER.info(
|
||||||
|
_payload(
|
||||||
|
session=session,
|
||||||
|
module=module,
|
||||||
|
action=action,
|
||||||
|
outcome=outcome,
|
||||||
|
target=target,
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def log_session_event(
|
||||||
|
session: UserSession | None,
|
||||||
|
*,
|
||||||
|
action: str,
|
||||||
|
outcome: str,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Write one session lifecycle event to the textual audit file."""
|
||||||
|
|
||||||
|
log_user_action(
|
||||||
|
session,
|
||||||
|
module="session",
|
||||||
|
action=action,
|
||||||
|
outcome=outcome,
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
8
barcode.pyw
Normal file
8
barcode.pyw
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||||
|
|
||||||
|
ensure_stdio("warehouse_barcode")
|
||||||
|
|
||||||
|
from barcode_client import main
|
||||||
|
|
||||||
|
|
||||||
|
raise SystemExit(run_with_fatal_log("Barcode WMS", main))
|
||||||
478
barcode_client.py
Normal file
478
barcode_client.py
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
"""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 runtime_support import ensure_stdio, run_with_fatal_log
|
||||||
|
|
||||||
|
ensure_stdio("warehouse_barcode")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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(run_with_fatal_log("Barcode WMS", main))
|
||||||
155
barcode_repository.py
Normal file
155
barcode_repository.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""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.py_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.py_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;
|
||||||
|
|
||||||
|
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||||
|
|
||||||
|
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 "")
|
||||||
86
busy_overlay.py
Normal file
86
busy_overlay.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
from ui_theme import theme_color, theme_font, theme_padding, theme_value
|
||||||
|
|
||||||
|
|
||||||
|
class InlineBusyOverlay:
|
||||||
|
"""Busy overlay rendered inside the same window, avoiding extra toplevels."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Misc, theme_cfg: dict[str, Any] | None = None):
|
||||||
|
self.parent = parent
|
||||||
|
self.theme_cfg = theme_cfg or {}
|
||||||
|
self._cover: ctk.CTkFrame | None = None
|
||||||
|
self._label: ctk.CTkLabel | None = None
|
||||||
|
self._bar: ctk.CTkProgressBar | None = None
|
||||||
|
|
||||||
|
def show(self, message: str = "Attendere..."):
|
||||||
|
if self._cover and self._cover.winfo_exists():
|
||||||
|
if self._label:
|
||||||
|
self._label.configure(text=message)
|
||||||
|
try:
|
||||||
|
self._cover.lift()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
cover = ctk.CTkFrame(
|
||||||
|
self.parent,
|
||||||
|
corner_radius=0,
|
||||||
|
fg_color=theme_color(self.theme_cfg, "overlay_cover_fg_color", ("#d9d9d9", "#4a4a4a")),
|
||||||
|
)
|
||||||
|
cover.place(relx=0, rely=0, relwidth=1, relheight=1)
|
||||||
|
try:
|
||||||
|
cover.lift()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
wrap = ctk.CTkFrame(
|
||||||
|
cover,
|
||||||
|
corner_radius=int(theme_value(self.theme_cfg, "overlay_panel_corner_radius", 10)),
|
||||||
|
fg_color=theme_color(self.theme_cfg, "overlay_panel_fg_color", ("#f2f2f2", "#353535")),
|
||||||
|
)
|
||||||
|
wrap.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
|
|
||||||
|
label = ctk.CTkLabel(
|
||||||
|
wrap,
|
||||||
|
text=message,
|
||||||
|
font=theme_font(self.theme_cfg, "overlay_label_font", ("Segoe UI", 11, "bold")),
|
||||||
|
)
|
||||||
|
label_pad = theme_padding(self.theme_cfg, "overlay_label_padding", (18, 14, 18, 8))
|
||||||
|
label.pack(padx=(label_pad[0], label_pad[2]), pady=(label_pad[1], label_pad[3]))
|
||||||
|
|
||||||
|
bar = ctk.CTkProgressBar(
|
||||||
|
wrap,
|
||||||
|
mode="indeterminate",
|
||||||
|
width=int(theme_value(self.theme_cfg, "overlay_progress_width", 220)),
|
||||||
|
)
|
||||||
|
bar_pad = theme_padding(self.theme_cfg, "overlay_progress_padding", (18, 0, 18, 14))
|
||||||
|
bar.pack(padx=(bar_pad[0], bar_pad[2]), pady=(bar_pad[1], bar_pad[3]))
|
||||||
|
try:
|
||||||
|
bar.start()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._cover = cover
|
||||||
|
self._label = label
|
||||||
|
self._bar = bar
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
if self._bar:
|
||||||
|
try:
|
||||||
|
self._bar.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._bar = None
|
||||||
|
self._label = None
|
||||||
|
if self._cover and self._cover.winfo_exists():
|
||||||
|
try:
|
||||||
|
self._cover.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._cover = None
|
||||||
69
checklist_test_campo_pickinglist.md
Normal file
69
checklist_test_campo_pickinglist.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Checklist Test Campo Picking List
|
||||||
|
|
||||||
|
## Deploy patch
|
||||||
|
|
||||||
|
1. Apri [apply_plist_reservation_patch.sql](C:/devel/python/ware_house/apply_plist_reservation_patch.sql) in SSMS.
|
||||||
|
2. Verifica di essere collegato al database corretto.
|
||||||
|
3. Esegui lo script completo.
|
||||||
|
4. Controlla che l'esecuzione termini senza errori SQL.
|
||||||
|
|
||||||
|
## Verifica oggetti DB
|
||||||
|
|
||||||
|
1. Verifica che esista la tabella `dbo.PickingListReservation`.
|
||||||
|
2. Verifica che siano stati aggiornati questi oggetti:
|
||||||
|
- `dbo.sp_xExePackingListPallet`
|
||||||
|
- `dbo.XMag_ViewPackingList`
|
||||||
|
- `dbo.sp_xExePackingListPalletPrenota`
|
||||||
|
- `dbo.sp_ControllaPrenotazionePackingListPalletNew`
|
||||||
|
3. Verifica che in `dbo.WarehouseObjectBackup` esistano le copie con tag `plist_reservation_fix_alpha2`.
|
||||||
|
|
||||||
|
## Test backoffice Gestione Picking List
|
||||||
|
|
||||||
|
1. Apri `Gestione Picking List`.
|
||||||
|
2. Verifica che la griglia alta mostri solo le UDC residue.
|
||||||
|
3. Prenota una picking list.
|
||||||
|
4. Verifica che risulti prenotata solo quella.
|
||||||
|
5. Premi di nuovo `Prenota` sulla stessa lista.
|
||||||
|
- Atteso: non cambia nulla.
|
||||||
|
6. Premi `S-prenota` sulla lista prenotata.
|
||||||
|
- Atteso: la lista passa a non prenotata.
|
||||||
|
7. Premi di nuovo `S-prenota`.
|
||||||
|
- Atteso: non cambia nulla.
|
||||||
|
8. Prenota una lista con UDC non scaffalate.
|
||||||
|
- Atteso: non devono accendersi altre liste per effetto della locazione `1000 / 5E1.1`.
|
||||||
|
9. Prenota una lista che in passato collideva sulla cella `8057`.
|
||||||
|
- Atteso: non deve più prenotare anche l'altra.
|
||||||
|
|
||||||
|
## Test barcode
|
||||||
|
|
||||||
|
1. Con una sola lista prenotata, premi `F1`.
|
||||||
|
- Atteso: viene proposta la coda alta della lista prenotata.
|
||||||
|
2. Premi `F2`.
|
||||||
|
- Atteso: viene proposta la coda bassa non prenotata.
|
||||||
|
3. Preleva una UDC della picking list.
|
||||||
|
- Atteso: compare `Ok Scarico`.
|
||||||
|
- Atteso: compare `7G.1.1`.
|
||||||
|
4. Se la UDC è non scaffalata:
|
||||||
|
- Atteso: compare `5E1.1`.
|
||||||
|
5. Dopo il prelievo, se restano altre UDC della stessa plist:
|
||||||
|
- Atteso: compare la UDC successiva.
|
||||||
|
|
||||||
|
## Test fine lista
|
||||||
|
|
||||||
|
1. Lavora una picking list fino all'ultima UDC.
|
||||||
|
2. Verifica che la plist sparisca dalla griglia alta.
|
||||||
|
3. Verifica che la prenotazione venga automaticamente disattivata.
|
||||||
|
4. Verifica che `F1` non continui più a considerarla attiva.
|
||||||
|
|
||||||
|
## Test Ricarica
|
||||||
|
|
||||||
|
1. Durante il lavoro su una plist, premi `Ricarica`.
|
||||||
|
2. Verifica che la griglia alta mostri solo il residuo reale.
|
||||||
|
3. Verifica che la plist esaurita non ricompaia.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
1. Apri [rollback_plist_reservation_patch.sql](C:/devel/python/ware_house/rollback_plist_reservation_patch.sql) in SSMS.
|
||||||
|
2. Verifica di essere sullo stesso database usato per il deploy.
|
||||||
|
3. Esegui lo script completo.
|
||||||
|
4. Controlla che il comportamento legacy sia ripristinato.
|
||||||
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
|
||||||
@@ -20,18 +20,18 @@ async_msssql_query.py
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
gestione_aree_frame_async.py
|
gestione_aree.py
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
.. automodule:: gestione_aree_frame_async
|
.. automodule:: gestione_aree
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
layout_window.py
|
gestione_layout.py
|
||||||
----------------
|
------------------
|
||||||
|
|
||||||
.. automodule:: layout_window
|
.. automodule:: gestione_layout
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
@@ -44,10 +44,10 @@ reset_corsie.py
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
view_celle_multiple.py
|
view_celle_multi_udc.py
|
||||||
----------------------
|
-----------------------
|
||||||
|
|
||||||
.. automodule:: view_celle_multiple
|
.. automodule:: view_celle_multi_udc
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ flowchart TD
|
|||||||
Main --> DB["AsyncMSSQLClient"]
|
Main --> DB["AsyncMSSQLClient"]
|
||||||
|
|
||||||
Launcher --> Reset["reset_corsie.py"]
|
Launcher --> Reset["reset_corsie.py"]
|
||||||
Launcher --> Layout["layout_window.py"]
|
Launcher --> Layout["gestione_layout.py"]
|
||||||
Launcher --> Ghost["view_celle_multiple.py"]
|
Launcher --> Ghost["view_celle_multi_udc.py"]
|
||||||
Launcher --> Search["search_pallets.py"]
|
Launcher --> Search["search_pallets.py"]
|
||||||
Launcher --> Picking["gestione_pickinglist.py"]
|
Launcher --> Picking["gestione_pickinglist.py"]
|
||||||
|
|
||||||
Reset --> Runner["gestione_aree_frame_async.AsyncRunner"]
|
Reset --> Runner["gestione_aree.AsyncRunner"]
|
||||||
Layout --> Runner
|
Layout --> Runner
|
||||||
Ghost --> Runner
|
Ghost --> Runner
|
||||||
Search --> Runner
|
Search --> Runner
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ I diagrammi sono scritti in Mermaid, quindi possono essere:
|
|||||||
## Indice
|
## Indice
|
||||||
|
|
||||||
- [main](./main_flow.md)
|
- [main](./main_flow.md)
|
||||||
- [layout_window](./layout_window_flow.md)
|
- [gestione_layout](./gestione_layout_flow.md)
|
||||||
- [reset_corsie](./reset_corsie_flow.md)
|
- [reset_corsie](./reset_corsie_flow.md)
|
||||||
- [view_celle_multiple](./view_celle_multiple_flow.md)
|
- [view_celle_multi_udc](./view_celle_multi_udc_flow.md)
|
||||||
- [search_pallets](./search_pallets_flow.md)
|
- [search_pallets](./search_pallets_flow.md)
|
||||||
- [gestione_pickinglist](./gestione_pickinglist_flow.md)
|
- [gestione_pickinglist](./gestione_pickinglist_flow.md)
|
||||||
- [infrastruttura async/db](./async_db_flow.md)
|
- [infrastruttura async/db](./async_db_flow.md)
|
||||||
|
- [warehouse operational flow](./warehouse_operational_flow.md)
|
||||||
|
|
||||||
## Convenzioni
|
## Convenzioni
|
||||||
|
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ flowchart TD
|
|||||||
## Note
|
## Note
|
||||||
|
|
||||||
- E un helper minimale usato da `main.py`.
|
- E un helper minimale usato da `main.py`.
|
||||||
- Il modulo esiste separato da `gestione_aree_frame_async.py`, ma concettualmente
|
- Il modulo esiste separato da `gestione_aree.py`, ma concettualmente
|
||||||
svolge lo stesso ruolo di gestione del loop condiviso.
|
svolge lo stesso ruolo di gestione del loop condiviso.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# `gestione_aree_frame_async.py`
|
# `gestione_aree.py`
|
||||||
|
|
||||||
## Scopo
|
## Scopo
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# `layout_window.py`
|
# `gestione_layout.py`
|
||||||
|
|
||||||
## Scopo
|
## Scopo
|
||||||
|
|
||||||
Questo modulo visualizza il layout delle corsie come matrice di celle, mostra
|
Questo modulo visualizza il layout delle corsie come matrice di celle, mostra
|
||||||
lo stato di occupazione, consente di cercare una UDC e permette l'export della
|
lo stato di occupazione, consente di cercare una UDC e permette l'export della
|
||||||
matrice.
|
matrice. La griglia ad alte prestazioni e' resa con `tksheet`, mantenendo la
|
||||||
|
stessa semantica visiva delle celle operative.
|
||||||
|
|
||||||
## Flusso operativo
|
## Flusso operativo
|
||||||
|
|
||||||
@@ -57,5 +58,7 @@ flowchart LR
|
|||||||
- Il modulo usa un token `_req_counter` per evitare che risposte async vecchie
|
- Il modulo usa un token `_req_counter` per evitare che risposte async vecchie
|
||||||
aggiornino la UI fuori ordine.
|
aggiornino la UI fuori ordine.
|
||||||
- La statistica globale viene ricalcolata da query SQL, mentre quella della
|
- La statistica globale viene ricalcolata da query SQL, mentre quella della
|
||||||
corsia corrente usa la matrice già caricata in memoria.
|
corsia corrente usa la matrice gia' caricata in memoria.
|
||||||
- `destroy()` marca la finestra come non più attiva per evitare callback tardive.
|
- Il click destro su una cella riusa lo stesso menu contestuale della versione
|
||||||
|
precedente basata su pulsanti CTk.
|
||||||
|
- `destroy()` marca la finestra come non piu' attiva per evitare callback tardive.
|
||||||
@@ -13,7 +13,7 @@ Questo modulo gestisce la vista master/detail delle picking list e permette di:
|
|||||||
|
|
||||||
```{mermaid}
|
```{mermaid}
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A["open_pickinglist_window() da main.py"] --> B["create_pickinglist_frame()"]
|
A["open_pickinglist_window() in gestione_pickinglist.py"] --> B["create_frame()"]
|
||||||
B --> C["GestionePickingListFrame.__init__()"]
|
B --> C["GestionePickingListFrame.__init__()"]
|
||||||
C --> D["_build_layout()"]
|
C --> D["_build_layout()"]
|
||||||
D --> E["after_idle(_first_show)"]
|
D --> E["after_idle(_first_show)"]
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ infrastrutturali.
|
|||||||
|
|
||||||
README.md
|
README.md
|
||||||
main_flow.md
|
main_flow.md
|
||||||
layout_window_flow.md
|
gestione_layout_flow.md
|
||||||
reset_corsie_flow.md
|
reset_corsie_flow.md
|
||||||
view_celle_multiple_flow.md
|
view_celle_multi_udc_flow.md
|
||||||
search_pallets_flow.md
|
search_pallets_flow.md
|
||||||
gestione_pickinglist_flow.md
|
gestione_pickinglist_flow.md
|
||||||
|
warehouse_operational_flow.md
|
||||||
async_db_flow.md
|
async_db_flow.md
|
||||||
async_msssql_query_flow.md
|
async_msssql_query_flow.md
|
||||||
gestione_aree_frame_async_flow.md
|
gestione_aree_flow.md
|
||||||
async_loop_singleton_flow.md
|
async_loop_singleton_flow.md
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ flowchart TD
|
|||||||
I --> K["open_layout_window()"]
|
I --> K["open_layout_window()"]
|
||||||
I --> L["open_celle_multiple_window()"]
|
I --> L["open_celle_multiple_window()"]
|
||||||
I --> M["open_search_window()"]
|
I --> M["open_search_window()"]
|
||||||
I --> N["open_pickinglist_window()"]
|
I --> N["gestione_pickinglist.open_pickinglist_window()"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schema di chiamata
|
## Schema di chiamata
|
||||||
@@ -33,13 +33,13 @@ flowchart LR
|
|||||||
Launcher --> Layout["open_layout_window"]
|
Launcher --> Layout["open_layout_window"]
|
||||||
Launcher --> Ghost["open_celle_multiple_window"]
|
Launcher --> Ghost["open_celle_multiple_window"]
|
||||||
Launcher --> Search["open_search_window"]
|
Launcher --> Search["open_search_window"]
|
||||||
Launcher --> Pick["open_pickinglist_window"]
|
Launcher --> Pick["gestione_pickinglist.open_pickinglist_window"]
|
||||||
Pick --> PickFactory["create_pickinglist_frame"]
|
Pick --> PickFactory["gestione_pickinglist.create_frame"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
|
|
||||||
- `db_app` viene creato una sola volta e poi passato a tutte le finestre.
|
- `db_app` viene creato una sola volta e poi passato a tutte le finestre.
|
||||||
- Alla chiusura del launcher viene chiamato `db_app.dispose()` sul loop globale.
|
- Alla chiusura del launcher viene chiamato `db_app.dispose()` sul loop globale.
|
||||||
- `open_pickinglist_window()` costruisce la finestra in modo nascosto e la rende
|
- `gestione_pickinglist.open_pickinglist_window()` costruisce la finestra in modo nascosto e la rende
|
||||||
visibile solo a layout pronto, per ridurre lo sfarfallio iniziale.
|
visibile solo a layout pronto, per ridurre lo sfarfallio iniziale.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# `view_celle_multiple.py`
|
# `view_celle_multi_udc.py`
|
||||||
|
|
||||||
## Scopo
|
## Scopo
|
||||||
|
|
||||||
167
docs/flows/warehouse_operational_flow.md
Normal file
167
docs/flows/warehouse_operational_flow.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Warehouse Operational Flow
|
||||||
|
|
||||||
|
Questo diagramma descrive il flusso operativo integrato tra:
|
||||||
|
|
||||||
|
- layout di magazzino;
|
||||||
|
- gestione picking list;
|
||||||
|
- movimentazione fisica dei pallet;
|
||||||
|
- viste SQL e stored procedure coinvolte;
|
||||||
|
- aggiornamento dell'interfaccia dopo ogni operazione.
|
||||||
|
|
||||||
|
## Vista d'insieme
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
flowchart TD
|
||||||
|
UI["Operatore su interfaccia"] --> LAY["Gestione Layout"]
|
||||||
|
UI --> PL["Gestione Picking List"]
|
||||||
|
|
||||||
|
subgraph Layout["Flusso Layout"]
|
||||||
|
LAY --> LC1["Caricamento corsie e metadati layout"]
|
||||||
|
LC1 --> LC2["vViewMappaturaDescrizioneCorsia"]
|
||||||
|
LC1 --> LC3["vViewMappaturaPosizCorsia"]
|
||||||
|
LC1 --> LC4["MagLayout"]
|
||||||
|
LC1 --> LC5["Celle / Magazzini"]
|
||||||
|
LC2 --> GRID["Rendering griglia scaffale"]
|
||||||
|
LC3 --> GRID
|
||||||
|
LC4 --> GRID
|
||||||
|
LC5 --> GRID
|
||||||
|
GRID --> LX["Click su cella / ricerca UDC / menu contestuale"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Picking["Flusso Picking List"]
|
||||||
|
PL --> PC1["Caricamento elenco documenti"]
|
||||||
|
PC1 --> PV1["XMag_ViewPackingList"]
|
||||||
|
PV1 --> PGRID["Griglia documenti aggregata"]
|
||||||
|
PGRID --> PSEL["Selezione documento"]
|
||||||
|
PSEL --> PV2["vViewPackingListRestante"]
|
||||||
|
PV2 --> PDET["Griglia dettaglio documento"]
|
||||||
|
PDET --> PACT["Prenota / S-prenota / consultazione"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Mov["Movimentazione pallet"]
|
||||||
|
LX --> MOVE{"Operazione fisica?"}
|
||||||
|
MOVE -->|Carico o spostamento| SP1["sp_xMagGestioneMagazziniPallet"]
|
||||||
|
MOVE -->|Scarico pallet| SP1
|
||||||
|
PACT -->|Prenota o s-prenota| SP2["sp_xExePackingListPallet"]
|
||||||
|
SP1 --> M1["Aggiorna movimenti MagazziniPallet"]
|
||||||
|
SP1 --> M2["Aggiorna cella destinazione / origine"]
|
||||||
|
SP1 --> M3["Controlla prenotazione automatica"]
|
||||||
|
M3 --> SP3["sp_ControllaPrenotazionePackingListPalletNew"]
|
||||||
|
SP3 --> SP4["sp_xExePackingListPalletPrenota"]
|
||||||
|
SP2 --> M4["Aggiorna Celle.IDStato"]
|
||||||
|
SP2 --> M5["Scrive LogPackingList"]
|
||||||
|
SP4 --> M4
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Views["Ricostruzione contesto"]
|
||||||
|
M1 --> GV["XMag_GiacenzaPallet"]
|
||||||
|
M2 --> CV["Celle"]
|
||||||
|
GV --> XP["XMag_ViewPackingList"]
|
||||||
|
GV --> TP["vXTracciaProdotti"]
|
||||||
|
CV --> XP
|
||||||
|
XP --> RL1["Stato documento / pallet / ubicazione"]
|
||||||
|
TP --> RL2["Articolo / lotto / descrizione"]
|
||||||
|
end
|
||||||
|
|
||||||
|
RL1 --> LREF["Refresh Layout / Picking List"]
|
||||||
|
RL2 --> LREF
|
||||||
|
LREF --> GRID
|
||||||
|
LREF --> PGRID
|
||||||
|
LREF --> PDET
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flusso del layout
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
flowchart TD
|
||||||
|
A["Apertura Gestione Layout"] --> B["Legge mappatura corsie"]
|
||||||
|
B --> B1["vViewMappaturaDescrizioneCorsia"]
|
||||||
|
B --> B2["vViewMappaturaPosizCorsia"]
|
||||||
|
B --> B3["MagLayout"]
|
||||||
|
B --> B4["Celle / Magazzini"]
|
||||||
|
B1 --> C["Costruisce geometria scaffale"]
|
||||||
|
B2 --> C
|
||||||
|
B3 --> C
|
||||||
|
B4 --> C
|
||||||
|
C --> D["Query giacenza pallet per corsia"]
|
||||||
|
D --> E["Colora celle: vuota / piena / multipla"]
|
||||||
|
E --> F["Mostra UDC piu recente nella cella"]
|
||||||
|
F --> G{"Interazione utente"}
|
||||||
|
G -->|Ricerca UDC| H["Evidenzia cella in blu"]
|
||||||
|
G -->|Tasto destro su cella rossa| I["Menu contestuale"]
|
||||||
|
I --> J["Dialog scarico / analisi multi UDC"]
|
||||||
|
J --> K["Movimentazione fisica pallet"]
|
||||||
|
K --> L["Refresh layout"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flusso della picking list
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
flowchart TD
|
||||||
|
A["Apertura Gestione Picking List"] --> B["Query aggregata documenti"]
|
||||||
|
B --> C["XMag_ViewPackingList"]
|
||||||
|
C --> D["Una riga per documento"]
|
||||||
|
D --> E["Utente seleziona il documento"]
|
||||||
|
E --> F["Query dettaglio documento"]
|
||||||
|
F --> G["vViewPackingListRestante"]
|
||||||
|
G --> H["Mostra pallet ancora rilevanti per il documento"]
|
||||||
|
H --> I{"Azione utente"}
|
||||||
|
I -->|Prenota o s-prenota| J["sp_xExePackingListPallet"]
|
||||||
|
I -->|Consulta dettaglio| H
|
||||||
|
J --> K["Aggiorna IDStato delle celle del documento"]
|
||||||
|
K --> L["LogPackingList"]
|
||||||
|
K --> M["Refresh lista documenti"]
|
||||||
|
K --> N["Refresh dettaglio documento"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flusso della movimentazione pallet
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
flowchart TD
|
||||||
|
A["Utente legge barcode pallet e cella"] --> B["sp_xMagGestioneMagazziniPallet"]
|
||||||
|
B --> C{"Il pallet esiste gia in giacenza?"}
|
||||||
|
C -->|No| D["Inserisce movimento V sulla nuova cella"]
|
||||||
|
C -->|Si| E["Inserisce movimento P sulla vecchia cella"]
|
||||||
|
E --> F["Inserisce movimento V sulla nuova cella"]
|
||||||
|
D --> G["Aggiorna ricostruzione giacenza"]
|
||||||
|
F --> G
|
||||||
|
G --> H["XMag_GiacenzaPallet"]
|
||||||
|
H --> I["XMag_ViewPackingList"]
|
||||||
|
H --> J["vXTracciaProdotti"]
|
||||||
|
I --> K["Ubicazione e stato documento"]
|
||||||
|
J --> L["Dati articolo e lotto"]
|
||||||
|
K --> M["Refresh interfaccia"]
|
||||||
|
L --> M
|
||||||
|
```
|
||||||
|
|
||||||
|
## Significato delle viste e delle stored procedure
|
||||||
|
|
||||||
|
- `XMag_ViewPackingList`:
|
||||||
|
ricostruisce il collegamento tra pallet, documento, ubicazione e stato
|
||||||
|
logistico. E' la vista principale per la schermata picking list.
|
||||||
|
- `vViewPackingListRestante`:
|
||||||
|
mostra il dettaglio operativo del documento, cioe' le righe ancora visibili
|
||||||
|
e ordinate per ubicazione.
|
||||||
|
- `vXTracciaProdotti`:
|
||||||
|
arricchisce il pallet con lotto, codice articolo e descrizione.
|
||||||
|
- `sp_xMagGestioneMagazziniPallet`:
|
||||||
|
esegue il movimento fisico del pallet nel magazzino.
|
||||||
|
- `sp_xExePackingListPallet`:
|
||||||
|
fa il toggle di prenotazione delle celle coinvolte in una picking list.
|
||||||
|
- `sp_xExePackingListPalletPrenota`:
|
||||||
|
forza la prenotazione a `IDStato = 1`.
|
||||||
|
- `sp_ControllaPrenotazionePackingListPalletNew`:
|
||||||
|
controlla se, dopo una movimentazione fisica, debba essere riapplicata una
|
||||||
|
prenotazione automatica sulle celle coinvolte.
|
||||||
|
|
||||||
|
## Lettura pratica del sistema
|
||||||
|
|
||||||
|
- Il layout risponde alla domanda:
|
||||||
|
"dove sono i pallet e come sono distribuiti nello scaffale?"
|
||||||
|
- La picking list risponde alla domanda:
|
||||||
|
"quali pallet fanno parte di un documento e quali celle sono prenotate?"
|
||||||
|
- La movimentazione pallet risponde alla domanda:
|
||||||
|
"cosa succede nel tracciato quando un pallet viene caricato, spostato o
|
||||||
|
scaricato?"
|
||||||
|
- Le viste vengono rilette dopo l'operazione per riportare la UI in uno stato
|
||||||
|
coerente con il database.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""One-off maintenance script to sanitize ``border_color`` usage in ``layout_window``.
|
"""One-off maintenance script to sanitize ``border_color`` usage in ``gestione_layout``.
|
||||||
|
|
||||||
The script removes incompatible ``border_color='transparent'`` assignments from
|
The script removes incompatible ``border_color='transparent'`` assignments from
|
||||||
widget configuration calls while preserving explicit highlight colors that are
|
widget configuration calls while preserving explicit highlight colors that are
|
||||||
@@ -9,7 +9,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Path default (modifica se serve)
|
# Path default (modifica se serve)
|
||||||
p = Path("./layout_window.py")
|
p = Path("./gestione_layout.py")
|
||||||
if not p.exists():
|
if not p.exists():
|
||||||
raise SystemExit(f"File non trovato: {p}")
|
raise SystemExit(f"File non trovato: {p}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""One-off maintenance script to patch performance issues in ``layout_window``.
|
"""One-off maintenance script to patch performance issues in ``gestione_layout``.
|
||||||
|
|
||||||
The script was used during development to remove an expensive resize-triggered
|
The script was used during development to remove an expensive resize-triggered
|
||||||
refresh and to inject some lifecycle guards into the window implementation.
|
refresh and to inject some lifecycle guards into the window implementation.
|
||||||
@@ -8,7 +8,7 @@ It is kept in the repository as an auditable patch recipe.
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
p = Path("./layout_window.py")
|
p = Path("./gestione_layout.py")
|
||||||
src = p.read_text(encoding="utf-8")
|
src = p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
backup = p.with_suffix(".py.bak_perf")
|
backup = p.with_suffix(".py.bak_perf")
|
||||||
|
|||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
"""Shared Tk/async helpers used by multiple warehouse windows.
|
"""Shared Tk/async helpers used by multiple warehouse windows.
|
||||||
|
|
||||||
The module bundles three concerns used throughout the GUI:
|
The module bundles two concerns used throughout the GUI:
|
||||||
|
|
||||||
* lifecycle of the shared background asyncio loop;
|
|
||||||
* a modal-like busy overlay shown during long-running tasks;
|
* a modal-like busy overlay shown during long-running tasks;
|
||||||
* an ``AsyncRunner`` that schedules coroutines and re-enters Tk safely.
|
* an ``AsyncRunner`` that schedules coroutines on the shared loop and
|
||||||
|
re-enters Tk safely.
|
||||||
|
|
||||||
|
The shared loop itself is defined only in :mod:`async_loop_singleton` and is
|
||||||
|
reused here instead of being recreated locally.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
__VERSION__ = "GestioneAreeFrame v3.2.5-singleloop"
|
from async_loop_singleton import get_global_loop
|
||||||
|
|
||||||
|
__VERSION__ = "GestioneAreeFrame v3.3.0-singleloop"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from async_msssql_query import AsyncMSSQLClient # noqa: F401
|
from async_msssql_query import AsyncMSSQLClient # noqa: F401
|
||||||
@@ -24,50 +28,6 @@ except Exception:
|
|||||||
AsyncMSSQLClient = object # type: ignore
|
AsyncMSSQLClient = object # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class _LoopHolder:
|
|
||||||
"""Keep references to the shared event loop and its worker thread."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
|
||||||
self.thread: Optional[threading.Thread] = None
|
|
||||||
self.ready = threading.Event()
|
|
||||||
|
|
||||||
|
|
||||||
_GLOBAL = _LoopHolder()
|
|
||||||
|
|
||||||
|
|
||||||
def _run_loop():
|
|
||||||
"""Create and run the shared event loop inside the worker thread."""
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
_GLOBAL.loop = loop
|
|
||||||
_GLOBAL.ready.set()
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
|
|
||||||
def get_global_loop() -> asyncio.AbstractEventLoop:
|
|
||||||
"""Return the shared background event loop, creating it if needed."""
|
|
||||||
if _GLOBAL.loop is not None:
|
|
||||||
return _GLOBAL.loop
|
|
||||||
_GLOBAL.thread = threading.Thread(target=_run_loop, name="warehouse-asyncio", daemon=True)
|
|
||||||
_GLOBAL.thread.start()
|
|
||||||
_GLOBAL.ready.wait(timeout=5.0)
|
|
||||||
if _GLOBAL.loop is None:
|
|
||||||
raise RuntimeError("Impossibile avviare l'event loop globale")
|
|
||||||
return _GLOBAL.loop
|
|
||||||
|
|
||||||
|
|
||||||
def stop_global_loop():
|
|
||||||
"""Stop the shared event loop and release thread references."""
|
|
||||||
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
|
||||||
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
|
||||||
if _GLOBAL.thread:
|
|
||||||
_GLOBAL.thread.join(timeout=2.0)
|
|
||||||
_GLOBAL.loop = None
|
|
||||||
_GLOBAL.thread = None
|
|
||||||
_GLOBAL.ready.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class BusyOverlay:
|
class BusyOverlay:
|
||||||
"""Semi-transparent overlay used to block interaction during async tasks."""
|
"""Semi-transparent overlay used to block interaction during async tasks."""
|
||||||
|
|
||||||
1442
gestione_layout.py
Normal file
1442
gestione_layout.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,72 @@ smooth by relying on deferred updates and lightweight progress indicators.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from typing import Optional, Any, Dict, List, Callable
|
from typing import Optional, Any, Dict, List, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from audit_log import log_user_action
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except Exception: # pragma: no cover - safety fallback if dependency is missing locally
|
||||||
|
class _FallbackLogger:
|
||||||
|
"""Minimal adapter used only when Loguru is not installed yet."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__)
|
||||||
|
self._logger.setLevel(logging.DEBUG)
|
||||||
|
self._logger.propagate = False
|
||||||
|
|
||||||
|
def bind(self, **_kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs):
|
||||||
|
handler: logging.Handler
|
||||||
|
if hasattr(sink, "write"):
|
||||||
|
handler = logging.StreamHandler(sink)
|
||||||
|
else:
|
||||||
|
handler = logging.FileHandler(str(sink), encoding=encoding)
|
||||||
|
handler.setLevel(getattr(logging, str(level).upper(), logging.INFO))
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
||||||
|
)
|
||||||
|
self._logger.addHandler(handler)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def log(self, level, message):
|
||||||
|
getattr(self._logger, str(level).lower(), self._logger.info)(message)
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def exception(self, message):
|
||||||
|
self._logger.exception(message)
|
||||||
|
|
||||||
|
logger = _FallbackLogger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from tksheet import Sheet, natural_sort_key
|
||||||
|
except Exception:
|
||||||
|
Sheet = None # type: ignore[assignment]
|
||||||
|
natural_sort_key = None # type: ignore[assignment]
|
||||||
|
|
||||||
# Usa overlay e runner "collaudati"
|
# Usa overlay e runner "collaudati"
|
||||||
from gestione_aree_frame_async 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
|
||||||
|
|
||||||
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
|
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -27,6 +85,118 @@ except Exception:
|
|||||||
self.rc = rc; self.message = message; self.id_result = id_result
|
self.rc = rc; self.message = message; self.id_result = id_result
|
||||||
|
|
||||||
|
|
||||||
|
PICKINGLIST_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG"
|
||||||
|
PICKINGLIST_DETAIL_TEST_MULTIPLIER = 1 # 1 disables artificial row expansion for UI stress tests
|
||||||
|
MODULE_LOG_NAME = Path(__file__).stem
|
||||||
|
MODULE_LOG_PATH = Path(__file__).with_suffix(".log")
|
||||||
|
_MODULE_LOG_ENABLED = PICKINGLIST_LOG_MODE.upper() != "OFF"
|
||||||
|
_MODULE_LOG_LEVEL = "DEBUG" if PICKINGLIST_LOG_MODE.upper() == "DEBUG" else "INFO"
|
||||||
|
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_module_logger():
|
||||||
|
"""Configure console and file logging for this module."""
|
||||||
|
global _MODULE_LOGGING_CONFIGURED
|
||||||
|
if _MODULE_LOGGING_CONFIGURED:
|
||||||
|
return
|
||||||
|
if not _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
sys.stderr,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=True,
|
||||||
|
filter=record_filter,
|
||||||
|
format=(
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>" + MODULE_LOG_NAME + "</cyan> | "
|
||||||
|
"<level>{message}</level>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
MODULE_LOG_PATH,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
filter=record_filter,
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}",
|
||||||
|
)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
|
||||||
|
|
||||||
|
def _format_payload(payload: Any) -> str:
|
||||||
|
"""Serialize payloads for human-readable logging."""
|
||||||
|
try:
|
||||||
|
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||||
|
except Exception:
|
||||||
|
return repr(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_call(level: Optional[str] = None):
|
||||||
|
"""Trace entry, exit and failure of selected high-level functions."""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
effective_level = level or _MODULE_LOG_LEVEL
|
||||||
|
_MODULE_LOGGER.log(
|
||||||
|
effective_level,
|
||||||
|
f"CALL {func.__qualname__} args={_format_payload(args[1:] if args else ())} kwargs={_format_payload(kwargs)}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
_MODULE_LOGGER.exception(f"FAIL {func.__qualname__}")
|
||||||
|
raise
|
||||||
|
_MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}")
|
||||||
|
return result
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _log_sql(query_name: str, sql: str, params: Dict[str, Any]):
|
||||||
|
"""Log one SQL statement and its parameters."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params)}")
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _log_dataset(query_name: str, rows: List[Dict[str, Any]]):
|
||||||
|
"""Log query results at summary or full-debug level depending on the flag."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows")
|
||||||
|
if PICKINGLIST_LOG_MODE.upper() == "DEBUG":
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_detail_rows_for_test(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""Artificially duplicate detail rows to stress-test the UI with larger datasets."""
|
||||||
|
multiplier = max(1, int(PICKINGLIST_DETAIL_TEST_MULTIPLIER))
|
||||||
|
if multiplier == 1 or not rows:
|
||||||
|
return rows
|
||||||
|
|
||||||
|
expanded: List[Dict[str, Any]] = []
|
||||||
|
for copy_idx in range(multiplier):
|
||||||
|
for row_idx, row in enumerate(rows):
|
||||||
|
cloned = dict(row)
|
||||||
|
cloned["__test_copy__"] = copy_idx
|
||||||
|
cloned["__test_row__"] = row_idx
|
||||||
|
expanded.append(cloned)
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Dataset dettaglio espanso artificialmente da {len(rows)} a {len(expanded)} righe per test UI"
|
||||||
|
)
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
|
_configure_module_logger()
|
||||||
|
if _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={PICKINGLIST_LOG_MODE.upper()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -------------------- SQL --------------------
|
# -------------------- SQL --------------------
|
||||||
SQL_PL = """
|
SQL_PL = """
|
||||||
SELECT
|
SELECT
|
||||||
@@ -45,14 +215,14 @@ SELECT
|
|||||||
MAX(Cella) AS Cella,
|
MAX(Cella) AS Cella,
|
||||||
MIN(Ordinamento) AS Ordinamento,
|
MIN(Ordinamento) AS Ordinamento,
|
||||||
MAX(IDStato) AS IDStato
|
MAX(IDStato) AS IDStato
|
||||||
FROM dbo.XMag_ViewPackingList
|
FROM dbo.py_ViewPackingListRestante
|
||||||
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||||
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SQL_PL_DETAILS = """
|
SQL_PL_DETAILS = """
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM ViewPackingListRestante
|
FROM dbo.py_ViewPackingListRestante
|
||||||
WHERE Documento = :Documento
|
WHERE Documento = :Documento
|
||||||
ORDER BY Ordinamento;
|
ORDER BY Ordinamento;
|
||||||
"""
|
"""
|
||||||
@@ -205,10 +375,18 @@ class ScrollTable(ctk.CTkFrame):
|
|||||||
PADX_R = 8
|
PADX_R = 8
|
||||||
PADY = 2
|
PADY = 2
|
||||||
|
|
||||||
def __init__(self, master, columns: List[ColSpec]):
|
def __init__(
|
||||||
|
self,
|
||||||
|
master,
|
||||||
|
columns: List[ColSpec],
|
||||||
|
on_header_click: Optional[Callable[[ColSpec], None]] = None,
|
||||||
|
):
|
||||||
"""Create a fixed-header scrollable table rendered with Tk/CTk widgets."""
|
"""Create a fixed-header scrollable table rendered with Tk/CTk widgets."""
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.columns = columns
|
self.columns = columns
|
||||||
|
self.on_header_click = on_header_click
|
||||||
|
self._sort_key: Optional[str] = None
|
||||||
|
self._sort_reverse = False
|
||||||
self.total_w = sum(c.width for c in self.columns)
|
self.total_w = sum(c.width for c in self.columns)
|
||||||
|
|
||||||
self.grid_rowconfigure(1, weight=1)
|
self.grid_rowconfigure(1, weight=1)
|
||||||
@@ -243,6 +421,10 @@ class ScrollTable(ctk.CTkFrame):
|
|||||||
# bind
|
# bind
|
||||||
self.h_inner.bind("<Configure>", lambda e: self._sync_header_width())
|
self.h_inner.bind("<Configure>", lambda e: self._sync_header_width())
|
||||||
self.b_inner.bind("<Configure>", lambda e: self._on_body_configure())
|
self.b_inner.bind("<Configure>", lambda e: self._on_body_configure())
|
||||||
|
self.b_canvas.bind("<Enter>", self._bind_mousewheel)
|
||||||
|
self.b_canvas.bind("<Leave>", self._unbind_mousewheel)
|
||||||
|
self.b_inner.bind("<Enter>", self._bind_mousewheel)
|
||||||
|
self.b_inner.bind("<Leave>", self._unbind_mousewheel)
|
||||||
|
|
||||||
self._build_header()
|
self._build_header()
|
||||||
|
|
||||||
@@ -265,12 +447,27 @@ class ScrollTable(ctk.CTkFrame):
|
|||||||
holder.pack(side="left", fill="y")
|
holder.pack(side="left", fill="y")
|
||||||
holder.pack_propagate(False)
|
holder.pack_propagate(False)
|
||||||
|
|
||||||
lbl = ctk.CTkLabel(holder, text=col.title, anchor="w")
|
header_text = col.title
|
||||||
|
if col.key == self._sort_key:
|
||||||
|
header_text = f"{col.title} {'↓' if self._sort_reverse else '↑'}"
|
||||||
|
|
||||||
|
lbl = ctk.CTkLabel(holder, text=header_text, anchor="w")
|
||||||
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
|
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
|
||||||
|
|
||||||
|
if self.on_header_click and col.key != "__check__":
|
||||||
|
for widget in (holder, lbl):
|
||||||
|
widget.bind("<Button-1>", lambda e, c=col: self.on_header_click(c))
|
||||||
|
widget.configure(cursor="hand2")
|
||||||
|
|
||||||
self.h_inner.configure(width=self.total_w, height=ROW_H)
|
self.h_inner.configure(width=self.total_w, height=ROW_H)
|
||||||
self.h_canvas.configure(scrollregion=(0,0,self.total_w,ROW_H))
|
self.h_canvas.configure(scrollregion=(0,0,self.total_w,ROW_H))
|
||||||
|
|
||||||
|
def set_sort_state(self, key: Optional[str], reverse: bool = False):
|
||||||
|
"""Update the header labels so the active sort is visible."""
|
||||||
|
self._sort_key = key
|
||||||
|
self._sort_reverse = reverse
|
||||||
|
self._build_header()
|
||||||
|
|
||||||
def _update_body_width(self):
|
def _update_body_width(self):
|
||||||
"""Keep the scroll region aligned with the current body content width."""
|
"""Keep the scroll region aligned with the current body content width."""
|
||||||
self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
|
self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
|
||||||
@@ -300,6 +497,22 @@ class ScrollTable(ctk.CTkFrame):
|
|||||||
self.h_canvas.xview_moveto(first)
|
self.h_canvas.xview_moveto(first)
|
||||||
self.xbar.set(first, last)
|
self.xbar.set(first, last)
|
||||||
|
|
||||||
|
def _bind_mousewheel(self, _event=None):
|
||||||
|
"""Route mouse-wheel scrolling to the body canvas while the cursor is over the table."""
|
||||||
|
self.b_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
|
||||||
|
|
||||||
|
def _unbind_mousewheel(self, _event=None):
|
||||||
|
"""Stop routing global mouse-wheel events when the pointer leaves the table."""
|
||||||
|
self.b_canvas.unbind_all("<MouseWheel>")
|
||||||
|
|
||||||
|
def _on_mousewheel(self, event):
|
||||||
|
"""Scroll the body canvas vertically in response to wheel movement."""
|
||||||
|
delta = getattr(event, "delta", 0)
|
||||||
|
if delta == 0:
|
||||||
|
return
|
||||||
|
step = -1 if delta > 0 else 1
|
||||||
|
self.b_canvas.yview_scroll(step, "units")
|
||||||
|
|
||||||
def clear_rows(self):
|
def clear_rows(self):
|
||||||
"""Remove all rendered body rows."""
|
"""Remove all rendered body rows."""
|
||||||
for w in self.b_inner.winfo_children():
|
for w in self.b_inner.winfo_children():
|
||||||
@@ -369,28 +582,51 @@ class PLRow:
|
|||||||
|
|
||||||
# -------------------- main frame (no-flicker + UX tuning + spinner) --------------------
|
# -------------------- main frame (no-flicker + UX tuning + spinner) --------------------
|
||||||
class GestionePickingListFrame(ctk.CTkFrame):
|
class GestionePickingListFrame(ctk.CTkFrame):
|
||||||
def __init__(self, master, *, db_client=None, conn_str=None):
|
@_log_call()
|
||||||
|
def __init__(self, master, *, db_client=None, conn_str=None, session: UserSession | None = None):
|
||||||
"""Create the master/detail picking list frame."""
|
"""Create the master/detail picking list frame."""
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
|
self._theme = theme_section("pickinglist_window", {})
|
||||||
|
self._locale_catalog = load_locale_catalog()
|
||||||
if db_client is None:
|
if db_client is None:
|
||||||
raise ValueError("GestionePickingListFrame richiede un db_client condiviso.")
|
raise ValueError("GestionePickingListFrame richiede un db_client condiviso.")
|
||||||
self.db_client = db_client
|
self.db_client = db_client
|
||||||
|
self.session = session
|
||||||
self.runner = AsyncRunner(self) # runner condiviso (usa loop globale)
|
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.rows_models: list[PLRow] = []
|
||||||
self._detail_cache: Dict[Any, list] = {}
|
self._detail_cache: Dict[Any, list] = {}
|
||||||
self.detail_doc = None
|
self.detail_doc = None
|
||||||
|
self._detail_sort_key: Optional[str] = None
|
||||||
|
self._detail_sort_reverse = False
|
||||||
|
self._detail_sorting = False
|
||||||
|
|
||||||
self._first_loading: bool = False # flag per cursore d'attesa solo al primo load
|
self._first_loading: bool = False # flag per cursore d'attesa solo al primo load
|
||||||
self._render_job = None # Tracking del job di rendering in corso
|
self._render_job = None # Tracking del job di rendering in corso
|
||||||
|
|
||||||
self._build_layout()
|
self._build_layout()
|
||||||
# 🔇 Niente reload immediato: carichiamo quando la finestra è idle (= già resa)
|
self._initial_load_started = False
|
||||||
self.after_idle(self._first_show)
|
|
||||||
|
def _can(self, action: str) -> bool:
|
||||||
|
"""Return whether the current user can execute one picking-list action."""
|
||||||
|
|
||||||
|
return self.session.can(action) if self.session else False
|
||||||
|
|
||||||
|
def _operator_id(self) -> int:
|
||||||
|
"""Return the authenticated operator id or ``0`` if no session is present."""
|
||||||
|
|
||||||
|
return int(self.session.operator_id) if self.session else 0
|
||||||
|
|
||||||
def _first_show(self):
|
def _first_show(self):
|
||||||
"""Chiamato a finestra già resa → evitiamo sfarfallio del primo paint e mostriamo wait-cursor."""
|
"""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
|
self._first_loading = True
|
||||||
try:
|
try:
|
||||||
self.winfo_toplevel().configure(cursor="watch")
|
self.winfo_toplevel().configure(cursor="watch")
|
||||||
@@ -408,13 +644,22 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
top = ctk.CTkFrame(self)
|
top = ctk.CTkFrame(self)
|
||||||
top.grid(row=0, column=0, sticky="ew", padx=10, pady=(8,4))
|
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([
|
for i, (text, cmd) in enumerate([
|
||||||
("Ricarica", self.reload_from_db),
|
(loc_text("picking.button.reload", catalog=self._locale_catalog, default="Ricarica"), self.reload_from_db),
|
||||||
("Prenota", self.on_prenota),
|
(loc_text("picking.button.prenota", catalog=self._locale_catalog, default="Prenota"), self.on_prenota),
|
||||||
("S-prenota", self.on_sprenota),
|
(loc_text("picking.button.sprenota", catalog=self._locale_catalog, default="S-prenota"), self.on_sprenota),
|
||||||
("Esporta XLSX", self.on_export)
|
(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 ---
|
# --- micro spinner a destra della toolbar ---
|
||||||
self.spinner = ToolbarSpinner(top)
|
self.spinner = ToolbarSpinner(top)
|
||||||
@@ -423,20 +668,171 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
self.pl_table = ScrollTable(self, PL_COLS)
|
self.pl_table = ScrollTable(self, PL_COLS)
|
||||||
self.pl_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(4,8))
|
self.pl_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(4,8))
|
||||||
|
|
||||||
self.det_table = ScrollTable(self, DET_COLS)
|
self.det_host = tk.Frame(self, bd=0, highlightthickness=0)
|
||||||
self.det_table.grid(row=3, column=0, sticky="nsew", padx=10, pady=(4,10))
|
self.det_host.grid(row=3, column=0, sticky="nsew", padx=10, pady=(4,10))
|
||||||
|
self.det_host.grid_rowconfigure(0, weight=1)
|
||||||
|
self.det_host.grid_columnconfigure(0, weight=1)
|
||||||
|
self._build_detail_sheet()
|
||||||
|
|
||||||
self._draw_details_hint()
|
self._draw_details_hint()
|
||||||
|
|
||||||
|
def _build_detail_sheet(self):
|
||||||
|
"""Create the high-volume detail table using tksheet."""
|
||||||
|
if Sheet is None:
|
||||||
|
raise RuntimeError("tksheet non disponibile: installa la dipendenza per usare la tabella dettagli.")
|
||||||
|
|
||||||
|
self.detail_sheet = Sheet(
|
||||||
|
self.det_host,
|
||||||
|
data=[],
|
||||||
|
show_row_index=False,
|
||||||
|
show_top_left=False,
|
||||||
|
width=1000,
|
||||||
|
height=320,
|
||||||
|
sort_key=natural_sort_key,
|
||||||
|
)
|
||||||
|
self.detail_sheet.change_theme("light green")
|
||||||
|
self.detail_sheet.enable_bindings("all")
|
||||||
|
self.detail_sheet.headers(self._detail_headers(), redraw=False)
|
||||||
|
self.detail_sheet.bind("<ButtonRelease-1>", self._on_detail_sheet_left_click, add="+")
|
||||||
|
self.detail_sheet.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
def _draw_details_hint(self):
|
def _draw_details_hint(self):
|
||||||
"""Render the placeholder row shown when no document is selected."""
|
"""Render the placeholder row shown when no document is selected."""
|
||||||
self.det_table.clear_rows()
|
self._load_detail_sheet_data(
|
||||||
self.det_table.add_row(
|
[["", "", "", "Seleziona una Picking List per vedere le UDC...", "", ""]]
|
||||||
values=["", "", "", "Seleziona una Picking List per vedere le UDC…", "", ""],
|
|
||||||
row_index=0,
|
|
||||||
anchors=["w"]*6
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _detail_headers(self) -> List[str]:
|
||||||
|
"""Return detail headers with the active sort indicator, if any."""
|
||||||
|
headers: List[str] = []
|
||||||
|
for col in DET_COLS:
|
||||||
|
title = col.title
|
||||||
|
if col.key == self._detail_sort_key:
|
||||||
|
title = f"{title} {'[desc]' if self._detail_sort_reverse else '[asc]'}"
|
||||||
|
headers.append(title)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _detail_rows_to_sheet_data(self, rows: List[Dict[str, Any]]) -> List[List[str]]:
|
||||||
|
"""Convert detail dictionaries to the row format expected by tksheet."""
|
||||||
|
data: List[List[str]] = []
|
||||||
|
for d in rows:
|
||||||
|
pallet = _s(_first(d, ["Pallet", "UDC", "PalletID"]))
|
||||||
|
lotto = _s(_first(d, ["Lotto"]))
|
||||||
|
articolo = _s(_first(d, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"]))
|
||||||
|
descr = _s(_first(d, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"]))
|
||||||
|
qta = _s(_first(d, ["Qta", "Quantita", "Qty", "QTY"]))
|
||||||
|
ubi_raw = _first(d, ["Ubicazione", "Cella", "PalletCella"])
|
||||||
|
loc = "Non scaffalata" if (ubi_raw is None or str(ubi_raw).strip() == "") else str(ubi_raw).strip()
|
||||||
|
data.append([pallet, lotto, articolo, descr, qta, loc])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _load_detail_sheet_data(self, data: List[List[str]]):
|
||||||
|
"""Push one full dataset into the tksheet detail widget."""
|
||||||
|
self.detail_sheet.headers(self._detail_headers(), redraw=False)
|
||||||
|
self.detail_sheet.set_sheet_data(
|
||||||
|
data,
|
||||||
|
reset_col_positions=True,
|
||||||
|
reset_row_positions=True,
|
||||||
|
redraw=True,
|
||||||
|
)
|
||||||
|
self.detail_sheet.set_all_column_widths()
|
||||||
|
|
||||||
|
def _detail_sort_value(self, row: Dict[str, Any], key: str):
|
||||||
|
"""Return a normalized value used to sort detail rows by one logical column."""
|
||||||
|
if key == "Ubicazione":
|
||||||
|
value = _first(row, ["Ubicazione", "Cella", "PalletCella"])
|
||||||
|
value = "Non scaffalata" if value in (None, "") else value
|
||||||
|
elif key == "Qta":
|
||||||
|
value = _first(row, ["Qta", "Quantita", "Qty", "QTY"], 0)
|
||||||
|
try:
|
||||||
|
return (0, float(value))
|
||||||
|
except Exception:
|
||||||
|
return (1, _s(value).lower())
|
||||||
|
elif key == "Articolo":
|
||||||
|
value = _first(row, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"])
|
||||||
|
elif key == "Descrizione":
|
||||||
|
value = _first(row, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"])
|
||||||
|
elif key == "Pallet":
|
||||||
|
value = _first(row, ["Pallet", "UDC", "PalletID"])
|
||||||
|
else:
|
||||||
|
value = row.get(key)
|
||||||
|
|
||||||
|
return (0, _s(value).lower())
|
||||||
|
|
||||||
|
def _sort_detail_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""Return detail rows sorted using the current header state."""
|
||||||
|
if not self._detail_sort_key:
|
||||||
|
return rows
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
rows,
|
||||||
|
key=lambda row: self._detail_sort_value(row, self._detail_sort_key),
|
||||||
|
reverse=self._detail_sort_reverse,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _finish_detail_sort_feedback(self, root: tk.Misc):
|
||||||
|
"""Dismiss busy feedback only after Tk has flushed the detail redraw."""
|
||||||
|
try:
|
||||||
|
root.update_idletasks()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._detail_sorting = False
|
||||||
|
self.spinner.stop()
|
||||||
|
self.busy.hide()
|
||||||
|
try:
|
||||||
|
self.detail_sheet.configure(cursor="")
|
||||||
|
root.configure(cursor="")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_detail_header_click(self, col: ColSpec):
|
||||||
|
"""Toggle detail sorting when the user clicks a detail header."""
|
||||||
|
if not self.detail_doc or self._detail_sorting:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._detail_sort_key == col.key:
|
||||||
|
self._detail_sort_reverse = not self._detail_sort_reverse
|
||||||
|
else:
|
||||||
|
self._detail_sort_key = col.key
|
||||||
|
self._detail_sort_reverse = False
|
||||||
|
|
||||||
|
self._detail_sorting = True
|
||||||
|
self.spinner.start(" Ordino dettagli...")
|
||||||
|
self.busy.show(f"Ordinamento per {col.title}...")
|
||||||
|
root = self.winfo_toplevel()
|
||||||
|
try:
|
||||||
|
root.configure(cursor="watch")
|
||||||
|
self.detail_sheet.configure(cursor="watch")
|
||||||
|
root.update_idletasks()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _apply_sort():
|
||||||
|
try:
|
||||||
|
rows = list(self._detail_cache.get(self.detail_doc, []))
|
||||||
|
rows = self._sort_detail_rows(rows)
|
||||||
|
self._detail_cache[self.detail_doc] = rows
|
||||||
|
self._refresh_details()
|
||||||
|
finally:
|
||||||
|
# Wait one more UI turn so the redraw becomes visible before removing feedback.
|
||||||
|
self.after_idle(lambda r=root: self.after(15, lambda: self._finish_detail_sort_feedback(r)))
|
||||||
|
|
||||||
|
self.after(25, _apply_sort)
|
||||||
|
|
||||||
|
def _on_detail_sheet_left_click(self, event):
|
||||||
|
"""Sort detail rows when the user clicks a tksheet header cell."""
|
||||||
|
try:
|
||||||
|
region = self.detail_sheet.identify_region(event)
|
||||||
|
column = self.detail_sheet.identify_column(event, exclude_header=False, allow_end=False)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if region != "header" or column is None or column < 0 or column >= len(DET_COLS):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._on_detail_header_click(DET_COLS[column])
|
||||||
|
|
||||||
def _apply_row_colors(self, rows: List[Dict[str, Any]]):
|
def _apply_row_colors(self, rows: List[Dict[str, Any]]):
|
||||||
"""Colorazione differita (after_idle) per evitare micro-jank durante l'inserimento righe."""
|
"""Colorazione differita (after_idle) per evitare micro-jank durante l'inserimento righe."""
|
||||||
try:
|
try:
|
||||||
@@ -517,6 +913,7 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# ----- eventi -----
|
# ----- eventi -----
|
||||||
|
@_log_call()
|
||||||
def on_row_checked(self, model: PLRow, is_checked: bool):
|
def on_row_checked(self, model: PLRow, is_checked: bool):
|
||||||
"""Handle row selection changes and refresh the detail section."""
|
"""Handle row selection changes and refresh the detail section."""
|
||||||
# selezione esclusiva
|
# selezione esclusiva
|
||||||
@@ -526,18 +923,23 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
m.set_checked(False)
|
m.set_checked(False)
|
||||||
|
|
||||||
self.detail_doc = model.pl.get("Documento")
|
self.detail_doc = model.pl.get("Documento")
|
||||||
|
_MODULE_LOGGER.info(f"Documento selezionato per il dettaglio: {self.detail_doc}")
|
||||||
self.spinner.start(" Carico dettagli…") # spinner ON
|
self.spinner.start(" Carico dettagli…") # spinner ON
|
||||||
|
|
||||||
async def _job():
|
async def _job():
|
||||||
|
_log_sql("SQL_PL_DETAILS", SQL_PL_DETAILS, {"Documento": self.detail_doc})
|
||||||
return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc})
|
return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc})
|
||||||
|
|
||||||
def _ok(res):
|
def _ok(res):
|
||||||
# NON fermare lo spinner subito: lo farà _refresh_details_incremental
|
# NON fermare lo spinner subito: lo farà _refresh_details_incremental
|
||||||
self._detail_cache[self.detail_doc] = _rows_to_dicts(res)
|
rows = _expand_detail_rows_for_test(_rows_to_dicts(res))
|
||||||
|
_log_dataset("SQL_PL_DETAILS", rows)
|
||||||
|
self._detail_cache[self.detail_doc] = rows
|
||||||
# Avvia il rendering incrementale che mantiene l'overlay attivo
|
# Avvia il rendering incrementale che mantiene l'overlay attivo
|
||||||
self._refresh_details_incremental()
|
self._refresh_details_incremental()
|
||||||
|
|
||||||
def _err(ex):
|
def _err(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore durante il caricamento dettagli del documento {self.detail_doc}: {ex}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
self.busy.hide() # Chiudi l'overlay in caso di errore
|
self.busy.hide() # Chiudi l'overlay in caso di errore
|
||||||
messagebox.showerror("DB", f"Errore nel caricamento dettagli:\n{ex}")
|
messagebox.showerror("DB", f"Errore nel caricamento dettagli:\n{ex}")
|
||||||
@@ -555,17 +957,23 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
else:
|
else:
|
||||||
if not any(m.is_checked() for m in self.rows_models):
|
if not any(m.is_checked() for m in self.rows_models):
|
||||||
self.detail_doc = None
|
self.detail_doc = None
|
||||||
|
_MODULE_LOGGER.info("Nessun documento selezionato: ripristino placeholder del dettaglio.")
|
||||||
self._refresh_details()
|
self._refresh_details()
|
||||||
|
|
||||||
# ----- load PL -----
|
# ----- load PL -----
|
||||||
def reload_from_db(self, first: bool = False):
|
@_log_call()
|
||||||
|
def reload_from_db(self, first: bool = False, reselect_documento: str | None = None):
|
||||||
"""Load or reload the picking list summary table from the database."""
|
"""Load or reload the picking list summary table from the database."""
|
||||||
self.spinner.start(" Carico…") # spinner ON
|
self.spinner.start(" Carico…") # spinner ON
|
||||||
async def _job():
|
async def _job():
|
||||||
|
_log_sql("SQL_PL", SQL_PL, {})
|
||||||
return await self.db_client.query_json(SQL_PL, {})
|
return await self.db_client.query_json(SQL_PL, {})
|
||||||
def _on_success(res):
|
def _on_success(res):
|
||||||
rows = _rows_to_dicts(res)
|
rows = _rows_to_dicts(res)
|
||||||
|
_log_dataset("SQL_PL", rows)
|
||||||
self._refresh_mid_rows(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
|
self.spinner.stop() # spinner OFF
|
||||||
# se era il primo load, ripristina il cursore standard
|
# se era il primo load, ripristina il cursore standard
|
||||||
if self._first_loading:
|
if self._first_loading:
|
||||||
@@ -575,6 +983,7 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
pass
|
pass
|
||||||
self._first_loading = False
|
self._first_loading = False
|
||||||
def _on_error(ex):
|
def _on_error(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore durante il caricamento della picking list: {ex}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
if self._first_loading:
|
if self._first_loading:
|
||||||
try:
|
try:
|
||||||
@@ -592,102 +1001,64 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
message="Caricamento Picking List…" if first else "Aggiornamento…"
|
message="Caricamento Picking List…" if first else "Aggiornamento…"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
def _refresh_details(self):
|
def _refresh_details(self):
|
||||||
"""Render the detail table for the currently selected document."""
|
"""Render the detail table for the currently selected document."""
|
||||||
self.det_table.clear_rows()
|
|
||||||
if not self.detail_doc:
|
if not self.detail_doc:
|
||||||
self._draw_details_hint()
|
self._draw_details_hint()
|
||||||
return
|
return
|
||||||
|
|
||||||
rows = self._detail_cache.get(self.detail_doc, [])
|
rows = list(self._detail_cache.get(self.detail_doc, []))
|
||||||
|
rows = self._sort_detail_rows(rows)
|
||||||
|
_MODULE_LOGGER.debug(f"Ridisegno tabella dettaglio per documento={self.detail_doc} righe={len(rows)}")
|
||||||
if not rows:
|
if not rows:
|
||||||
self.det_table.add_row(values=["", "", "", "Nessuna UDC trovata.", "", ""],
|
self._load_detail_sheet_data([["", "", "", "Nessuna UDC trovata.", "", ""]])
|
||||||
row_index=0, anchors=["w"]*6)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for r, d in enumerate(rows):
|
self._load_detail_sheet_data(self._detail_rows_to_sheet_data(rows))
|
||||||
pallet = _s(_first(d, ["Pallet", "UDC", "PalletID"]))
|
|
||||||
lotto = _s(_first(d, ["Lotto"]))
|
|
||||||
articolo = _s(_first(d, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"]))
|
|
||||||
descr = _s(_first(d, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"]))
|
|
||||||
qta = _s(_first(d, ["Qta", "Quantita", "Qty", "QTY"]))
|
|
||||||
ubi_raw = _first(d, ["Ubicazione", "Cella", "PalletCella"])
|
|
||||||
loc = "Non scaffalata" if (ubi_raw is None or str(ubi_raw).strip()=="") else str(ubi_raw).strip()
|
|
||||||
|
|
||||||
self.det_table.add_row(
|
|
||||||
values=[pallet, lotto, articolo, descr, qta, loc],
|
|
||||||
row_index=r,
|
|
||||||
anchors=[c.anchor for c in DET_COLS]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
def _refresh_details_incremental(self, batch_size: int = 25):
|
def _refresh_details_incremental(self, batch_size: int = 25):
|
||||||
"""
|
"""
|
||||||
Render detail table incrementally in batches to keep UI responsive.
|
Render detail table using tksheet while keeping busy feedback consistent.
|
||||||
Mantiene l'overlay visibile fino al completamento del rendering.
|
|
||||||
"""
|
"""
|
||||||
self.det_table.clear_rows()
|
|
||||||
if not self.detail_doc:
|
if not self.detail_doc:
|
||||||
self._draw_details_hint()
|
self._draw_details_hint()
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
self.busy.hide()
|
self.busy.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
rows = self._detail_cache.get(self.detail_doc, [])
|
rows = list(self._detail_cache.get(self.detail_doc, []))
|
||||||
|
rows = self._sort_detail_rows(rows)
|
||||||
|
self._detail_cache[self.detail_doc] = rows
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Avvio rendering dettagli documento={self.detail_doc} righe={len(rows)}")
|
||||||
if not rows:
|
if not rows:
|
||||||
self.det_table.add_row(values=["", "", "", "Nessuna UDC trovata.", "", ""],
|
self._load_detail_sheet_data([["", "", "", "Nessuna UDC trovata.", "", ""]])
|
||||||
row_index=0, anchors=["w"]*6)
|
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
self.busy.hide()
|
self.busy.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Inizia il rendering incrementale
|
|
||||||
total_rows = len(rows)
|
|
||||||
self.busy.show(f"Rendering {len(rows)} UDC...")
|
self.busy.show(f"Rendering {len(rows)} UDC...")
|
||||||
self._render_batch(rows, batch_size, 0, total_rows)
|
self._load_detail_sheet_data(self._detail_rows_to_sheet_data(rows))
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Rendering dettagli completato documento={self.detail_doc} righe={len(rows)}")
|
||||||
def _render_batch(self, rows: List[Dict[str, Any]], batch_size: int, start_idx: int, total_rows: int):
|
|
||||||
"""
|
|
||||||
Render a batch of rows and schedule the next batch.
|
|
||||||
Mantiene lo spinner attivo fino all'ultimo batch.
|
|
||||||
"""
|
|
||||||
end_idx = min(start_idx + batch_size, total_rows)
|
|
||||||
|
|
||||||
# Aggiorna lo spinner con il progresso
|
|
||||||
progress_pct = int((end_idx / total_rows) * 100)
|
|
||||||
self.spinner.lbl.configure(text=f"◐ Rendering {progress_pct}%")
|
|
||||||
|
|
||||||
# Aggiorna anche il messaggio dell'overlay
|
|
||||||
self.busy.show(f"Rendering {progress_pct}% ({end_idx}/{total_rows} UDC)...")
|
|
||||||
|
|
||||||
# Renderizza il batch corrente
|
|
||||||
for r in range(start_idx, end_idx):
|
|
||||||
d = rows[r]
|
|
||||||
pallet = _s(_first(d, ["Pallet", "UDC", "PalletID"]))
|
|
||||||
lotto = _s(_first(d, ["Lotto"]))
|
|
||||||
articolo = _s(_first(d, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"]))
|
|
||||||
descr = _s(_first(d, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"]))
|
|
||||||
qta = _s(_first(d, ["Qta", "Quantita", "Qty", "QTY"]))
|
|
||||||
ubi_raw = _first(d, ["Ubicazione", "Cella", "PalletCella"])
|
|
||||||
loc = "Non scaffalata" if (ubi_raw is None or str(ubi_raw).strip()=="") else str(ubi_raw).strip()
|
|
||||||
|
|
||||||
self.det_table.add_row(
|
|
||||||
values=[pallet, lotto, articolo, descr, qta, loc],
|
|
||||||
row_index=r,
|
|
||||||
anchors=[c.anchor for c in DET_COLS]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Se ci sono ancora righe da renderizzare, schedula il prossimo batch
|
|
||||||
if end_idx < total_rows:
|
|
||||||
# Lascia respirare Tk tra i batch (10ms)
|
|
||||||
self.after(10, lambda: self._render_batch(rows, batch_size, end_idx, total_rows))
|
|
||||||
else:
|
|
||||||
# Ultimo batch completato: ferma lo spinner e chiudi l'overlay
|
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
self.busy.hide()
|
self.busy.hide()
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
|
def _render_batch(self, rows: List[Dict[str, Any]], batch_size: int, start_idx: int, total_rows: int):
|
||||||
|
"""
|
||||||
|
Legacy helper kept for compatibility after the move to tksheet.
|
||||||
|
"""
|
||||||
|
del batch_size, start_idx, total_rows
|
||||||
|
self._load_detail_sheet_data(self._detail_rows_to_sheet_data(rows))
|
||||||
|
|
||||||
# ----- azioni -----
|
# ----- azioni -----
|
||||||
|
@_log_call()
|
||||||
def on_prenota(self):
|
def on_prenota(self):
|
||||||
"""Reserve the selected picking list."""
|
"""Reserve the selected picking list."""
|
||||||
|
if not self._can("pickinglist.prenota"):
|
||||||
|
messagebox.showwarning("Permesso negato", "L'operatore corrente non puo' prenotare picking list.", parent=self)
|
||||||
|
return
|
||||||
model = self._get_selected_model()
|
model = self._get_selected_model()
|
||||||
if not model:
|
if not model:
|
||||||
messagebox.showinfo("Prenota", "Seleziona una Picking List (checkbox) prima di prenotare.")
|
messagebox.showinfo("Prenota", "Seleziona una Picking List (checkbox) prima di prenotare.")
|
||||||
@@ -700,22 +1071,52 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
messagebox.showinfo("Prenota", f"La Picking List {documento} è già prenotata.")
|
messagebox.showinfo("Prenota", f"La Picking List {documento} è già prenotata.")
|
||||||
return
|
return
|
||||||
|
|
||||||
id_operatore = 1 # TODO: recupera dal contesto reale
|
id_operatore = self._operator_id()
|
||||||
|
if id_operatore <= 0:
|
||||||
|
messagebox.showerror("Prenota", "Sessione operatore non valida.", parent=self)
|
||||||
|
return
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Richiesta prenotazione documento={documento} id_operatore={id_operatore}")
|
||||||
self.spinner.start(" Prenoto…")
|
self.spinner.start(" Prenoto…")
|
||||||
|
|
||||||
async def _job():
|
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):
|
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)}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
if res and res.rc == 0:
|
if res and res.rc == 0:
|
||||||
self._recolor_row_by_documento(documento, desired)
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.prenota",
|
||||||
|
outcome="ok",
|
||||||
|
target=documento,
|
||||||
|
)
|
||||||
|
self._detail_cache.pop(documento, None)
|
||||||
|
self.reload_from_db(reselect_documento=documento)
|
||||||
else:
|
else:
|
||||||
msg = (res.message if res else "Errore sconosciuto")
|
msg = (res.message if res else "Errore sconosciuto")
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.prenota",
|
||||||
|
outcome="denied",
|
||||||
|
target=documento,
|
||||||
|
details={"message": msg},
|
||||||
|
)
|
||||||
messagebox.showerror("Prenota", f"Operazione non riuscita:\n{msg}")
|
messagebox.showerror("Prenota", f"Operazione non riuscita:\n{msg}")
|
||||||
|
|
||||||
def _err(ex):
|
def _err(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore prenotazione documento={documento}: {ex}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.prenota",
|
||||||
|
outcome="error",
|
||||||
|
target=documento,
|
||||||
|
details={"error": str(ex)},
|
||||||
|
)
|
||||||
messagebox.showerror("Prenota", f"Errore:\n{ex}")
|
messagebox.showerror("Prenota", f"Errore:\n{ex}")
|
||||||
|
|
||||||
self.runner.run(
|
self.runner.run(
|
||||||
@@ -726,8 +1127,12 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
message=f"Prenoto la Picking List {documento}…"
|
message=f"Prenoto la Picking List {documento}…"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def on_sprenota(self):
|
def on_sprenota(self):
|
||||||
"""Unreserve the selected picking list."""
|
"""Unreserve the selected picking list."""
|
||||||
|
if not self._can("pickinglist.sprenota"):
|
||||||
|
messagebox.showwarning("Permesso negato", "L'operatore corrente non puo' s-prenotare picking list.", parent=self)
|
||||||
|
return
|
||||||
model = self._get_selected_model()
|
model = self._get_selected_model()
|
||||||
if not model:
|
if not model:
|
||||||
messagebox.showinfo("S-prenota", "Seleziona una Picking List (checkbox) prima di s-prenotare.")
|
messagebox.showinfo("S-prenota", "Seleziona una Picking List (checkbox) prima di s-prenotare.")
|
||||||
@@ -740,22 +1145,52 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
messagebox.showinfo("S-prenota", f"La Picking List {documento} è già NON prenotata.")
|
messagebox.showinfo("S-prenota", f"La Picking List {documento} è già NON prenotata.")
|
||||||
return
|
return
|
||||||
|
|
||||||
id_operatore = 1 # TODO: recupera dal contesto reale
|
id_operatore = self._operator_id()
|
||||||
|
if id_operatore <= 0:
|
||||||
|
messagebox.showerror("S-prenota", "Sessione operatore non valida.", parent=self)
|
||||||
|
return
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Richiesta s-prenotazione documento={documento} id_operatore={id_operatore}")
|
||||||
self.spinner.start(" S-prenoto…")
|
self.spinner.start(" S-prenoto…")
|
||||||
|
|
||||||
async def _job():
|
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):
|
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)}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
if res and res.rc == 0:
|
if res and res.rc == 0:
|
||||||
self._recolor_row_by_documento(documento, desired)
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.sprenota",
|
||||||
|
outcome="ok",
|
||||||
|
target=documento,
|
||||||
|
)
|
||||||
|
self._detail_cache.pop(documento, None)
|
||||||
|
self.reload_from_db(reselect_documento=documento)
|
||||||
else:
|
else:
|
||||||
msg = (res.message if res else "Errore sconosciuto")
|
msg = (res.message if res else "Errore sconosciuto")
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.sprenota",
|
||||||
|
outcome="denied",
|
||||||
|
target=documento,
|
||||||
|
details={"message": msg},
|
||||||
|
)
|
||||||
messagebox.showerror("S-prenota", f"Operazione non riuscita:\n{msg}")
|
messagebox.showerror("S-prenota", f"Operazione non riuscita:\n{msg}")
|
||||||
|
|
||||||
def _err(ex):
|
def _err(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore s-prenotazione documento={documento}: {ex}")
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="pickinglist.sprenota",
|
||||||
|
outcome="error",
|
||||||
|
target=documento,
|
||||||
|
details={"error": str(ex)},
|
||||||
|
)
|
||||||
messagebox.showerror("S-prenota", f"Errore:\n{ex}")
|
messagebox.showerror("S-prenota", f"Errore:\n{ex}")
|
||||||
|
|
||||||
self.runner.run(
|
self.runner.run(
|
||||||
@@ -772,10 +1207,72 @@ class GestionePickingListFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
|
|
||||||
# factory per main
|
# factory per main
|
||||||
def create_frame(parent, *, db_client=None, conn_str=None) -> 'GestionePickingListFrame':
|
@_log_call()
|
||||||
|
def create_frame(parent, *, db_client=None, conn_str=None, session: UserSession | None = None) -> 'GestionePickingListFrame':
|
||||||
"""Factory used by the launcher to build the picking list frame."""
|
"""Factory used by the launcher to build the picking list frame."""
|
||||||
ctk.set_appearance_mode("light")
|
ctk.set_appearance_mode("light")
|
||||||
ctk.set_default_color_theme("green")
|
ctk.set_default_color_theme("green")
|
||||||
return GestionePickingListFrame(parent, db_client=db_client)
|
return GestionePickingListFrame(parent, db_client=db_client, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def open_pickinglist_window(parent: tk.Misc, db_client, session: UserSession | None = None) -> tk.Misc:
|
||||||
|
"""Open the picking list window while minimizing the first paint flicker."""
|
||||||
|
key = "_gestione_pickinglist_window_singleton"
|
||||||
|
ex = getattr(parent, key, None)
|
||||||
|
if ex and ex.winfo_exists():
|
||||||
|
frame = getattr(ex, "_pickinglist_frame", None)
|
||||||
|
if frame is not None:
|
||||||
|
try:
|
||||||
|
frame.session = session
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ex.deiconify()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ex.lift()
|
||||||
|
ex.focus_force()
|
||||||
|
return ex
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
win = ctk.CTkToplevel(parent)
|
||||||
|
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.
|
||||||
|
try:
|
||||||
|
win.withdraw()
|
||||||
|
win.attributes("-alpha", 0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
frame = create_frame(win, db_client=db_client, session=session)
|
||||||
|
try:
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
setattr(win, "_pickinglist_frame", frame)
|
||||||
|
|
||||||
|
# Reveal the fully-laid out window only after pending geometry work completes.
|
||||||
|
try:
|
||||||
|
win.update_idletasks()
|
||||||
|
place_window_fullsize_below_parent_later(parent, win)
|
||||||
|
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
|
||||||
|
|
||||||
|
win.bind("<Escape>", lambda e: win.destroy())
|
||||||
|
win.protocol("WM_DELETE_WINDOW", win.destroy)
|
||||||
|
return win
|
||||||
|
|
||||||
# =================== /gestione_pickinglist.py ===================
|
# =================== /gestione_pickinglist.py ===================
|
||||||
|
|||||||
772
gestione_scarico.py
Normal file
772
gestione_scarico.py
Normal file
@@ -0,0 +1,772 @@
|
|||||||
|
"""Modal dialog used to unload one or more UDCs from a multi-occupancy cell."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
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:
|
||||||
|
from loguru import logger
|
||||||
|
except Exception: # pragma: no cover - fallback used only when Loguru is not available
|
||||||
|
class _FallbackLogger:
|
||||||
|
"""Minimal adapter used only when Loguru is not installed yet."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__)
|
||||||
|
self._logger.setLevel(logging.DEBUG)
|
||||||
|
self._logger.propagate = False
|
||||||
|
|
||||||
|
def bind(self, **_kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs):
|
||||||
|
handler: logging.Handler
|
||||||
|
if hasattr(sink, "write"):
|
||||||
|
handler = logging.StreamHandler(sink)
|
||||||
|
else:
|
||||||
|
handler = logging.FileHandler(str(sink), encoding=encoding)
|
||||||
|
handler.setLevel(getattr(logging, str(level).upper(), logging.INFO))
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
||||||
|
)
|
||||||
|
self._logger.addHandler(handler)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def log(self, level, message):
|
||||||
|
getattr(self._logger, str(level).lower(), self._logger.info)(message)
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def exception(self, message):
|
||||||
|
self._logger.exception(message)
|
||||||
|
|
||||||
|
logger = _FallbackLogger()
|
||||||
|
|
||||||
|
|
||||||
|
SCARICO_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG"
|
||||||
|
MODULE_LOG_NAME = Path(__file__).stem
|
||||||
|
MODULE_LOG_PATH = Path(__file__).with_suffix(".log")
|
||||||
|
_MODULE_LOG_ENABLED = SCARICO_LOG_MODE.upper() != "OFF"
|
||||||
|
_MODULE_LOG_LEVEL = "DEBUG" if SCARICO_LOG_MODE.upper() == "DEBUG" else "INFO"
|
||||||
|
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = False
|
||||||
|
DEFAULT_SCARICO_USER = "warehouse_ui"
|
||||||
|
|
||||||
|
|
||||||
|
def _session_login(session: UserSession | None, fallback: str | None = None) -> str:
|
||||||
|
"""Return the current application login, falling back to a technical user."""
|
||||||
|
|
||||||
|
if session and str(session.login or "").strip():
|
||||||
|
return str(session.login).strip()
|
||||||
|
return str((fallback or DEFAULT_SCARICO_USER) or DEFAULT_SCARICO_USER).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_module_logger():
|
||||||
|
"""Configure console and file logging for this module."""
|
||||||
|
global _MODULE_LOGGING_CONFIGURED
|
||||||
|
if _MODULE_LOGGING_CONFIGURED:
|
||||||
|
return
|
||||||
|
if not _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME
|
||||||
|
logger.add(
|
||||||
|
sys.stderr,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=True,
|
||||||
|
filter=record_filter,
|
||||||
|
format=(
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>" + MODULE_LOG_NAME + "</cyan> | "
|
||||||
|
"<level>{message}</level>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
MODULE_LOG_PATH,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
filter=record_filter,
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}",
|
||||||
|
)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
|
||||||
|
|
||||||
|
def _format_payload(payload: Any) -> str:
|
||||||
|
"""Serialize payloads for human-readable logging."""
|
||||||
|
try:
|
||||||
|
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||||
|
except Exception:
|
||||||
|
return repr(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_call(level: str | None = None):
|
||||||
|
"""Trace entry, exit and failure of selected high-level functions."""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
effective_level = level or _MODULE_LOG_LEVEL
|
||||||
|
_MODULE_LOGGER.log(
|
||||||
|
effective_level,
|
||||||
|
f"CALL {func.__qualname__} args={_format_payload(args[1:] if args else ())} kwargs={_format_payload(kwargs)}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
_MODULE_LOGGER.exception(f"FAIL {func.__qualname__}")
|
||||||
|
raise
|
||||||
|
_MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _log_sql(query_name: str, sql: str, params: dict[str, Any]):
|
||||||
|
"""Log one SQL statement and its parameters."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params)}")
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _log_dataset(query_name: str, rows: list[Any]):
|
||||||
|
"""Log query results at summary or full-debug level depending on the flag."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows")
|
||||||
|
if SCARICO_LOG_MODE.upper() == "DEBUG":
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}")
|
||||||
|
|
||||||
|
|
||||||
|
_configure_module_logger()
|
||||||
|
if _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={SCARICO_LOG_MODE.upper()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SQL_UDC_IN_CELLA = """
|
||||||
|
WITH cell_pallets AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
g.BarcodePallet
|
||||||
|
FROM dbo.XMag_GiacenzaPallet g
|
||||||
|
WHERE g.IDCella = :idcella
|
||||||
|
),
|
||||||
|
last_in_cell AS (
|
||||||
|
SELECT
|
||||||
|
mp.Attributo AS BarcodePallet,
|
||||||
|
MAX(mp.ID) AS LastID
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN cell_pallets cp
|
||||||
|
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||||
|
WHERE mp.IDCella = :idcella
|
||||||
|
AND mp.Tipo = 'V'
|
||||||
|
GROUP BY mp.Attributo
|
||||||
|
),
|
||||||
|
last_move AS (
|
||||||
|
SELECT
|
||||||
|
mp.Attributo AS BarcodePallet,
|
||||||
|
mp.ID,
|
||||||
|
mp.DataMagazzino
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN last_in_cell lic ON lic.LastID = mp.ID
|
||||||
|
),
|
||||||
|
latest_any AS (
|
||||||
|
SELECT
|
||||||
|
ranked.BarcodePallet,
|
||||||
|
ranked.IDCella
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
mp.Attributo AS BarcodePallet,
|
||||||
|
mp.IDCella,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY mp.Attributo
|
||||||
|
ORDER BY mp.ID DESC
|
||||||
|
) AS rn
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN cell_pallets cp
|
||||||
|
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||||
|
WHERE mp.Tipo = 'V'
|
||||||
|
AND mp.PesoUnitario > 0
|
||||||
|
) ranked
|
||||||
|
WHERE ranked.rn = 1
|
||||||
|
),
|
||||||
|
shipped AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
shipped.BarcodePallet
|
||||||
|
FROM dbo.XMag_GiacenzaPalletPlistChiuse shipped
|
||||||
|
JOIN cell_pallets cp
|
||||||
|
ON cp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
shipped.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cp.BarcodePallet AS UDC,
|
||||||
|
lm.ID AS SourceID,
|
||||||
|
lm.DataMagazzino AS LastEventAt,
|
||||||
|
CASE
|
||||||
|
WHEN shipped.BarcodePallet IS NOT NULL THEN CAST(1 AS int)
|
||||||
|
ELSE CAST(0 AS int)
|
||||||
|
END AS IsShippedGhost,
|
||||||
|
CASE
|
||||||
|
WHEN la.IDCella IS NOT NULL
|
||||||
|
AND la.IDCella <> :idcella
|
||||||
|
THEN CAST(1 AS int)
|
||||||
|
ELSE CAST(0 AS int)
|
||||||
|
END AS IsMovedGhost
|
||||||
|
FROM cell_pallets cp
|
||||||
|
LEFT JOIN last_move lm
|
||||||
|
ON lm.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
LEFT JOIN latest_any la
|
||||||
|
ON la.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
LEFT JOIN shipped
|
||||||
|
ON shipped.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
cp.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
ORDER BY
|
||||||
|
lm.ID DESC,
|
||||||
|
cp.BarcodePallet DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SQL_SCARICA_UDC = """
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Now datetime = GETDATE();
|
||||||
|
DECLARE @SourceID int = 0;
|
||||||
|
DECLARE @NumeroPallet int = 0;
|
||||||
|
DECLARE @PesoUnitario float = 1;
|
||||||
|
DECLARE @Tara float = 0;
|
||||||
|
DECLARE @SourceIDCella int = 0;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@SourceID = src.ID,
|
||||||
|
@NumeroPallet = ISNULL(src.NumeroPallet, 0),
|
||||||
|
@PesoUnitario = ISNULL(NULLIF(src.PesoUnitario, 0), 1),
|
||||||
|
@Tara = ISNULL(src.Tara, 0),
|
||||||
|
@SourceIDCella = ISNULL(src.IDCella, 0)
|
||||||
|
FROM dbo.MagazziniPallet src
|
||||||
|
WHERE src.Attributo = :barcode_pallet
|
||||||
|
AND src.Tipo = 'V'
|
||||||
|
AND src.PesoUnitario > 0
|
||||||
|
ORDER BY src.ID DESC;
|
||||||
|
|
||||||
|
IF @SourceID > 0
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.MagazziniPallet
|
||||||
|
SET ModUtente = :utente,
|
||||||
|
ModDataOra = @Now
|
||||||
|
WHERE ID = @SourceID;
|
||||||
|
|
||||||
|
INSERT INTO dbo.MagazziniPallet (
|
||||||
|
Tipo,
|
||||||
|
IDRiferimento,
|
||||||
|
NumeroPallet,
|
||||||
|
Attributo,
|
||||||
|
IDMagazzino,
|
||||||
|
IDArea,
|
||||||
|
IDCella,
|
||||||
|
DataMagazzino,
|
||||||
|
PesoUnitario,
|
||||||
|
Tara,
|
||||||
|
InsUtente,
|
||||||
|
InsDataOra
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'P',
|
||||||
|
@SourceID,
|
||||||
|
ISNULL(src.NumeroPallet, 0),
|
||||||
|
src.Attributo,
|
||||||
|
src.IDMagazzino,
|
||||||
|
src.IDArea,
|
||||||
|
src.IDCella,
|
||||||
|
@Now,
|
||||||
|
ISNULL(NULLIF(src.PesoUnitario, 0), 1),
|
||||||
|
ISNULL(src.Tara, 0),
|
||||||
|
:utente,
|
||||||
|
@Now
|
||||||
|
FROM dbo.MagazziniPallet src
|
||||||
|
WHERE src.ID = @SourceID;
|
||||||
|
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = :utente,
|
||||||
|
ModDataOra = @Now
|
||||||
|
WHERE ID = @SourceIDCella;
|
||||||
|
END;
|
||||||
|
|
||||||
|
INSERT INTO dbo.MagazziniPallet (
|
||||||
|
Tipo,
|
||||||
|
IDRiferimento,
|
||||||
|
NumeroPallet,
|
||||||
|
Attributo,
|
||||||
|
IDMagazzino,
|
||||||
|
IDArea,
|
||||||
|
IDCella,
|
||||||
|
DataMagazzino,
|
||||||
|
PesoUnitario,
|
||||||
|
Tara,
|
||||||
|
InsUtente,
|
||||||
|
InsDataOra
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'V',
|
||||||
|
@SourceID,
|
||||||
|
@NumeroPallet,
|
||||||
|
:barcode_pallet,
|
||||||
|
target.IDMagazzino,
|
||||||
|
target.IDArea,
|
||||||
|
target.ID,
|
||||||
|
@Now,
|
||||||
|
@PesoUnitario,
|
||||||
|
@Tara,
|
||||||
|
:utente,
|
||||||
|
@Now
|
||||||
|
FROM (
|
||||||
|
SELECT c.ID, c.IDArea, a.IDMagazzino
|
||||||
|
FROM dbo.Celle c
|
||||||
|
JOIN dbo.Aree a ON a.ID = c.IDArea
|
||||||
|
WHERE c.ID = :target_idcella
|
||||||
|
) AS target;
|
||||||
|
|
||||||
|
EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
CAST(1 AS int) AS Ok,
|
||||||
|
@SourceID AS SourceID,
|
||||||
|
:target_idcella AS TargetIDCella,
|
||||||
|
:target_barcode_cella AS TargetBarcodeCella;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def move_pallet_async(
|
||||||
|
db_client,
|
||||||
|
*,
|
||||||
|
barcode_pallet: str,
|
||||||
|
target_idcella: int,
|
||||||
|
target_barcode_cella: str,
|
||||||
|
utente: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Move one pallet to a target cell using the same movement semantics as the legacy app.
|
||||||
|
|
||||||
|
The original C# application delegates load, transfer and unload to
|
||||||
|
``sp_xMagGestioneMagazziniPallet``. The Python app mirrors the same
|
||||||
|
behavior with an explicit SQL batch that:
|
||||||
|
1. finds the latest positive row for the pallet,
|
||||||
|
2. registers a compensating ``P`` move on the source cell,
|
||||||
|
3. frees the previous cell reservation,
|
||||||
|
4. inserts a new ``V`` move on the target cell,
|
||||||
|
5. re-runs the packing-list reservation check.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"barcode_pallet": str(barcode_pallet or "").strip(),
|
||||||
|
"target_idcella": int(target_idcella),
|
||||||
|
"target_barcode_cella": str(target_barcode_cella or "").strip(),
|
||||||
|
"utente": str((utente or DEFAULT_SCARICO_USER) or "warehouse_ui").strip(),
|
||||||
|
}
|
||||||
|
_log_sql("move_pallet", SQL_SCARICA_UDC, params)
|
||||||
|
res = await db_client.query_json(SQL_SCARICA_UDC, params, commit=True)
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
_log_dataset("move_pallet", rows)
|
||||||
|
first = rows[0] if rows else [1, 0, params["target_idcella"], params["target_barcode_cella"]]
|
||||||
|
return {
|
||||||
|
"ok": int(first[0] or 0),
|
||||||
|
"source_id": int(first[1] or 0),
|
||||||
|
"target_idcella": int(first[2] or 0),
|
||||||
|
"target_barcode_cella": str(first[3] or ""),
|
||||||
|
"barcode_pallet": params["barcode_pallet"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScaricoRow:
|
||||||
|
"""View-model describing one unloadable UDC currently present in a cell."""
|
||||||
|
|
||||||
|
udc: str
|
||||||
|
source_id: int | None
|
||||||
|
last_event_at: str
|
||||||
|
diagnostic_note: str
|
||||||
|
selected: tk.BooleanVar
|
||||||
|
|
||||||
|
|
||||||
|
def _build_diagnostic_note(is_shipped: int | bool, is_moved: int | bool) -> str:
|
||||||
|
"""Translate low-level anomaly flags into one operator-facing note."""
|
||||||
|
|
||||||
|
notes: list[str] = []
|
||||||
|
if bool(is_shipped):
|
||||||
|
notes.append("Mancato scarico: spedita")
|
||||||
|
if bool(is_moved):
|
||||||
|
notes.append("Mancato scarico: spostata")
|
||||||
|
return " | ".join(notes)
|
||||||
|
|
||||||
|
|
||||||
|
class ScaricoDialog(ctk.CTkToplevel):
|
||||||
|
"""Modal dialog that allows unloading selected UDCs from one cell."""
|
||||||
|
|
||||||
|
CHECKBOX_COL_W = 56
|
||||||
|
UDC_COL_W = 130
|
||||||
|
DATE_COL_W = 180
|
||||||
|
DIAG_COL_W = 320
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: tk.Misc,
|
||||||
|
*,
|
||||||
|
db_client,
|
||||||
|
idcella: int,
|
||||||
|
ubicazione: str,
|
||||||
|
on_completed: Callable[[], None] | None = None,
|
||||||
|
session: UserSession | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.db_client = db_client
|
||||||
|
self.idcella = idcella
|
||||||
|
self.ubicazione = ubicazione
|
||||||
|
self.on_completed = on_completed
|
||||||
|
self.session = session
|
||||||
|
self.rows: list[ScaricoRow] = []
|
||||||
|
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(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)
|
||||||
|
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
self.grid_rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._load_rows()
|
||||||
|
|
||||||
|
self.update_idletasks()
|
||||||
|
self.grab_set()
|
||||||
|
try:
|
||||||
|
self.wait_visibility()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.lift()
|
||||||
|
self.focus_force()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
"""Build the compact modal layout."""
|
||||||
|
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=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=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)
|
||||||
|
)
|
||||||
|
|
||||||
|
table = ctk.CTkFrame(self)
|
||||||
|
table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 8))
|
||||||
|
table.grid_rowconfigure(0, weight=1)
|
||||||
|
table.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
tree_host = tk.Frame(table, bd=0, highlightthickness=0, background="#DBDBDB")
|
||||||
|
tree_host.grid(row=0, column=0, sticky="nsew", padx=8, pady=(8, 8))
|
||||||
|
tree_host.grid_rowconfigure(0, weight=1)
|
||||||
|
tree_host.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
style = ttk.Style(self)
|
||||||
|
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,
|
||||||
|
columns=("sel", "udc", "last", "diag"),
|
||||||
|
show="headings",
|
||||||
|
style="Scarico.Treeview",
|
||||||
|
selectmode="none",
|
||||||
|
)
|
||||||
|
self.rows_tree.heading("sel", text="Sel")
|
||||||
|
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")
|
||||||
|
self.rows_tree.column("diag", width=self.DIAG_COL_W, stretch=True, anchor="w")
|
||||||
|
self.rows_tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.rows_tree.bind("<Button-1>", self._on_tree_click, add="+")
|
||||||
|
|
||||||
|
tree_scroll = ttk.Scrollbar(tree_host, orient="vertical", command=self.rows_tree.yview)
|
||||||
|
tree_scroll.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.rows_tree.configure(yscrollcommand=tree_scroll.set)
|
||||||
|
|
||||||
|
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=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=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
|
||||||
|
)
|
||||||
|
|
||||||
|
def _render_rows(self):
|
||||||
|
"""Recreate the compact list of unloadable UDC rows."""
|
||||||
|
if self.rows_tree is None:
|
||||||
|
return
|
||||||
|
for item in self.rows_tree.get_children():
|
||||||
|
self.rows_tree.delete(item)
|
||||||
|
|
||||||
|
for idx, row in enumerate(self.rows):
|
||||||
|
self.rows_tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
iid=str(idx),
|
||||||
|
values=(
|
||||||
|
"[x]" if row.selected.get() else "[ ]",
|
||||||
|
row.udc,
|
||||||
|
row.last_event_at,
|
||||||
|
row.diagnostic_note or "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.update_idletasks()
|
||||||
|
width = max(900, min(1040, self.winfo_reqwidth() + 20))
|
||||||
|
total_height = max(210, min(460, self.winfo_reqheight() + 8))
|
||||||
|
self.geometry(f"{width}x{total_height}")
|
||||||
|
|
||||||
|
def _on_tree_click(self, event):
|
||||||
|
"""Toggle the pseudo-checkbox when the operator clicks the first column."""
|
||||||
|
|
||||||
|
if self.rows_tree is None:
|
||||||
|
return
|
||||||
|
region = self.rows_tree.identify("region", event.x, event.y)
|
||||||
|
if region != "cell":
|
||||||
|
return
|
||||||
|
column = self.rows_tree.identify_column(event.x)
|
||||||
|
item_id = self.rows_tree.identify_row(event.y)
|
||||||
|
if column != "#1" or not item_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
row = self.rows[int(item_id)]
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
row.selected.set(not row.selected.get())
|
||||||
|
self.rows_tree.set(item_id, "sel", "[x]" if row.selected.get() else "[ ]")
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def _load_rows(self):
|
||||||
|
"""Load the list of current UDCs ordered from newest to oldest."""
|
||||||
|
params = {"idcella": self.idcella}
|
||||||
|
_log_sql("scarico_load_rows", SQL_UDC_IN_CELLA, params)
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
_log_dataset("scarico_load_rows", rows)
|
||||||
|
self.rows = []
|
||||||
|
for udc, source_id, last_event_at, is_shipped, is_moved in rows:
|
||||||
|
if isinstance(last_event_at, datetime):
|
||||||
|
last_event = last_event_at.strftime("%d/%m/%Y %H:%M:%S")
|
||||||
|
else:
|
||||||
|
last_event = str(last_event_at or "")
|
||||||
|
self.rows.append(
|
||||||
|
ScaricoRow(
|
||||||
|
udc=str(udc or ""),
|
||||||
|
source_id=int(source_id) if source_id is not None else None,
|
||||||
|
last_event_at=last_event,
|
||||||
|
diagnostic_note=_build_diagnostic_note(is_shipped, is_moved),
|
||||||
|
selected=tk.BooleanVar(value=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._render_rows()
|
||||||
|
self._busy.hide()
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
self._busy.hide()
|
||||||
|
_MODULE_LOGGER.exception(f"Errore caricamento righe scarico idcella={self.idcella}: {ex}")
|
||||||
|
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(
|
||||||
|
self.db_client.query_json(SQL_UDC_IN_CELLA, params),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
busy=self._busy,
|
||||||
|
message="Carico UDC...",
|
||||||
|
)
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def _on_scarica(self):
|
||||||
|
"""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(
|
||||||
|
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(
|
||||||
|
"Conferma scarico",
|
||||||
|
f"Scaricare {len(selected)} UDC da {self.ubicazione}?",
|
||||||
|
parent=self,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
for row in selected:
|
||||||
|
result = await move_pallet_async(
|
||||||
|
self.db_client,
|
||||||
|
barcode_pallet=row.udc,
|
||||||
|
target_idcella=9999,
|
||||||
|
target_barcode_cella="9000000",
|
||||||
|
utente=_session_login(self.session),
|
||||||
|
)
|
||||||
|
results.append({"udc": row.udc, "affected": int(result.get("ok") or 0)})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _ok(results):
|
||||||
|
_log_dataset("scarica_udc", results)
|
||||||
|
done = [item["udc"] for item in results if int(item.get("affected") or 0) > 0]
|
||||||
|
skipped = [item["udc"] for item in results if int(item.get("affected") or 0) <= 0]
|
||||||
|
if not done:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Scarica",
|
||||||
|
"Nessuna UDC e' stata scaricata. Verifica che le unita' siano ancora presenti in cella.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if skipped:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Scarica",
|
||||||
|
"Scarico parziale.\nCompletate: "
|
||||||
|
+ ", ".join(done)
|
||||||
|
+ "\nNon scaricate: "
|
||||||
|
+ ", ".join(skipped),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Scarica",
|
||||||
|
"Scarico completato per:\n" + "\n".join(done),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="layout.scarico",
|
||||||
|
outcome="ok",
|
||||||
|
target=self.ubicazione,
|
||||||
|
details={"scaricate": done, "saltate": skipped},
|
||||||
|
)
|
||||||
|
if self.on_completed:
|
||||||
|
self.on_completed()
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore scarico idcella={self.idcella}: {ex}")
|
||||||
|
log_user_action(
|
||||||
|
self.session,
|
||||||
|
module=MODULE_LOG_NAME,
|
||||||
|
action="layout.scarico",
|
||||||
|
outcome="error",
|
||||||
|
target=self.ubicazione,
|
||||||
|
details={"error": str(ex)},
|
||||||
|
)
|
||||||
|
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(),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
busy=self._busy,
|
||||||
|
message="Scarico UDC...",
|
||||||
|
)
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def _close(self):
|
||||||
|
"""Release the modal grab and close the dialog safely."""
|
||||||
|
try:
|
||||||
|
self.grab_release()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
|
def open_scarico_dialog(
|
||||||
|
parent: tk.Misc,
|
||||||
|
*,
|
||||||
|
db_client,
|
||||||
|
idcella: int,
|
||||||
|
ubicazione: str,
|
||||||
|
on_completed: Callable[[], None] | None = None,
|
||||||
|
session: UserSession | None = None,
|
||||||
|
) -> ScaricoDialog:
|
||||||
|
"""Create and return the modal unload dialog for one multi-UDC cell."""
|
||||||
|
return ScaricoDialog(
|
||||||
|
parent,
|
||||||
|
db_client=db_client,
|
||||||
|
idcella=idcella,
|
||||||
|
ubicazione=ubicazione,
|
||||||
|
on_completed=on_completed,
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
698
layout_window.py
698
layout_window.py
@@ -1,698 +0,0 @@
|
|||||||
"""Graphical aisle layout viewer for warehouse cells and pallet occupancy."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import Menu, messagebox, filedialog
|
|
||||||
import customtkinter as ctk
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
|
||||||
|
|
||||||
# ---- Color palette ----
|
|
||||||
COLOR_EMPTY = "#B0B0B0" # grigio (vuota)
|
|
||||||
COLOR_FULL = "#FFA500" # arancione (una UDC)
|
|
||||||
COLOR_DOUBLE = "#D62728" # rosso (>=2 UDC)
|
|
||||||
FG_DARK = "#111111"
|
|
||||||
FG_LIGHT = "#FFFFFF"
|
|
||||||
|
|
||||||
|
|
||||||
def pct_text(p_full: float, p_double: float | None = None) -> str:
|
|
||||||
"""Format occupancy percentages for the progress-bar labels."""
|
|
||||||
p_full = max(0.0, min(1.0, p_full))
|
|
||||||
pf = round(p_full * 100, 1)
|
|
||||||
pe = round(100 - pf, 1)
|
|
||||||
if p_double and p_double > 0:
|
|
||||||
pd = round(p_double * 100, 1)
|
|
||||||
return f"Pieno {pf}% · Vuoto {pe}% (di cui doppie {pd}%)"
|
|
||||||
return f"Pieno {pf}% · Vuoto {pe}%"
|
|
||||||
|
|
||||||
|
|
||||||
class LayoutWindow(ctk.CTkToplevel):
|
|
||||||
"""
|
|
||||||
Visualizzazione layout corsie con matrice di celle.
|
|
||||||
- Ogni cella è un pulsante colorato (vuota/piena/doppia)
|
|
||||||
- Etichetta su DUE righe:
|
|
||||||
1) "Corsia.Colonna.Fila" (una sola riga, senza andare a capo)
|
|
||||||
2) barcode UDC (primo, se presente)
|
|
||||||
- Ricerca per barcode UDC con cambio automatico corsia + highlight cella
|
|
||||||
- Statistiche: globale e corsia selezionata
|
|
||||||
- Export XLSX
|
|
||||||
"""
|
|
||||||
def __init__(self, parent: tk.Widget, db_app):
|
|
||||||
"""Create the window and initialize the state used by the matrix view."""
|
|
||||||
super().__init__(parent)
|
|
||||||
self.title("Warehouse · Layout corsie")
|
|
||||||
self.geometry("1200x740")
|
|
||||||
self.minsize(980, 560)
|
|
||||||
self.resizable(True, True)
|
|
||||||
|
|
||||||
self.db = db_app
|
|
||||||
self._busy = BusyOverlay(self)
|
|
||||||
self._async = AsyncRunner(self)
|
|
||||||
|
|
||||||
# layout principale 5% / 80% / 15%
|
|
||||||
self.grid_rowconfigure(0, weight=5)
|
|
||||||
self.grid_rowconfigure(1, weight=80)
|
|
||||||
self.grid_rowconfigure(2, weight=15)
|
|
||||||
self.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# stato runtime
|
|
||||||
self.corsia_selezionata = tk.StringVar()
|
|
||||||
self.buttons: list[list[ctk.CTkButton]] = []
|
|
||||||
self.btn_frames: list[list[ctk.CTkFrame]] = []
|
|
||||||
self.matrix_state: list[list[int]] = [] # <— rinominata: prima era self.state
|
|
||||||
self.fila_txt: list[list[str]] = []
|
|
||||||
self.col_txt: list[list[str]] = []
|
|
||||||
self.desc: list[list[str]] = []
|
|
||||||
self.udc1: list[list[str]] = [] # primo barcode UDC trovato (o "")
|
|
||||||
|
|
||||||
# ricerca → focus differito (corsia, col, fila, barcode)
|
|
||||||
self._pending_focus: tuple[str, str, str, str] | None = None
|
|
||||||
self._highlighted: tuple[int, int] | None = None
|
|
||||||
|
|
||||||
# anti-race: token per ignorare risposte vecchie
|
|
||||||
self._req_counter = 0
|
|
||||||
self._last_req = 0
|
|
||||||
self._alive = True
|
|
||||||
self._stats_after_id = None # se mai userai un refresh periodico, potremo cancellarlo qui
|
|
||||||
|
|
||||||
self._build_top()
|
|
||||||
self._build_matrix_host()
|
|
||||||
self._build_stats()
|
|
||||||
|
|
||||||
self._load_corsie()
|
|
||||||
# disabilitato: il refresh ad ogni <Configure> generava molte query/lag
|
|
||||||
# self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))
|
|
||||||
|
|
||||||
# ---------------- TOP BAR ----------------
|
|
||||||
def _build_top(self):
|
|
||||||
"""Create the top toolbar with aisle selection and search controls."""
|
|
||||||
top = ctk.CTkFrame(self)
|
|
||||||
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=6)
|
|
||||||
for i in range(4):
|
|
||||||
top.grid_columnconfigure(i, weight=0)
|
|
||||||
top.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
# lista corsie
|
|
||||||
lf = ctk.CTkFrame(top)
|
|
||||||
lf.grid(row=0, column=0, sticky="nsw")
|
|
||||||
lf.grid_columnconfigure(0, weight=1)
|
|
||||||
ctk.CTkLabel(lf, text="Corsie", font=("", 12, "bold")).grid(row=0, column=0, sticky="w", padx=6, pady=(6, 2))
|
|
||||||
self.lb = tk.Listbox(lf, height=6, exportselection=False)
|
|
||||||
self.lb.grid(row=1, column=0, sticky="nsw", padx=6, pady=(0, 6))
|
|
||||||
self.lb.bind("<<ListboxSelect>>", self._on_select)
|
|
||||||
|
|
||||||
# search by barcode
|
|
||||||
srch = ctk.CTkFrame(top)
|
|
||||||
srch.grid(row=0, column=1, sticky="nsew", padx=(10, 10))
|
|
||||||
self.search_var = tk.StringVar()
|
|
||||||
self.search_entry = ctk.CTkEntry(srch, textvariable=self.search_var, width=260)
|
|
||||||
self.search_entry.grid(row=0, column=0, sticky="w")
|
|
||||||
ctk.CTkButton(srch, text="Cerca per barcode UDC", command=self._search_udc).grid(row=0, column=1, padx=(8, 0))
|
|
||||||
srch.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# toolbar
|
|
||||||
tb = ctk.CTkFrame(top)
|
|
||||||
tb.grid(row=0, column=3, sticky="ne")
|
|
||||||
ctk.CTkButton(tb, text="Aggiorna", command=self._refresh_current).grid(row=0, column=0, padx=4)
|
|
||||||
ctk.CTkButton(tb, text="Export XLSX", command=self._export_xlsx).grid(row=0, column=1, padx=4)
|
|
||||||
|
|
||||||
# ---------------- MATRIX HOST ----------------
|
|
||||||
def _build_matrix_host(self):
|
|
||||||
"""Create the container that will host the dynamically rebuilt matrix."""
|
|
||||||
center = ctk.CTkFrame(self)
|
|
||||||
center.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 6))
|
|
||||||
center.grid_rowconfigure(0, weight=1)
|
|
||||||
center.grid_columnconfigure(0, weight=1)
|
|
||||||
self.host = ctk.CTkFrame(center)
|
|
||||||
self.host.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
|
|
||||||
|
|
||||||
def _apply_cell_style(self, btn: ctk.CTkButton, state: int):
|
|
||||||
"""Apply the visual state associated with a cell occupancy level."""
|
|
||||||
if state == 0:
|
|
||||||
btn.configure(
|
|
||||||
fg_color=COLOR_EMPTY, hover_color="#9A9A9A",
|
|
||||||
text_color=FG_DARK, border_width=0
|
|
||||||
)
|
|
||||||
elif state == 1:
|
|
||||||
btn.configure(
|
|
||||||
fg_color=COLOR_FULL, hover_color="#E69500",
|
|
||||||
text_color=FG_DARK, border_width=0
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
btn.configure(
|
|
||||||
fg_color=COLOR_DOUBLE, hover_color="#B22222",
|
|
||||||
text_color=FG_LIGHT, border_width=0
|
|
||||||
)
|
|
||||||
|
|
||||||
def _clear_highlight(self):
|
|
||||||
"""Remove the temporary highlight from the previously focused cell."""
|
|
||||||
if self._highlighted and self.buttons:
|
|
||||||
r, c = self._highlighted
|
|
||||||
try:
|
|
||||||
if 0 <= r < len(self.buttons) and 0 <= c < len(self.buttons[r]):
|
|
||||||
btn = self.buttons[r][c]
|
|
||||||
if getattr(btn, "winfo_exists", None) and btn.winfo_exists():
|
|
||||||
try:
|
|
||||||
btn.configure(border_width=0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# clear blue frame border
|
|
||||||
try:
|
|
||||||
fr = self.btn_frames[r][c]
|
|
||||||
if fr and getattr(fr, "winfo_exists", None) and fr.winfo_exists():
|
|
||||||
fr.configure(border_width=0)
|
|
||||||
# in CTkFrame non esiste highlightthickness come in tk; border_* è corretto
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._highlighted = None
|
|
||||||
|
|
||||||
def _rebuild_matrix(self, rows: int, cols: int, state, fila_txt, col_txt, desc, udc1, corsia):
|
|
||||||
"""Recreate the visible cell matrix from the latest query result."""
|
|
||||||
# prima rimuovi highlight su vecchi bottoni
|
|
||||||
self._clear_highlight()
|
|
||||||
# ripulisci host
|
|
||||||
for w in self.host.winfo_children():
|
|
||||||
w.destroy()
|
|
||||||
self.buttons.clear()
|
|
||||||
self.btn_frames.clear()
|
|
||||||
|
|
||||||
# salva matrici
|
|
||||||
self.matrix_state, self.fila_txt, self.col_txt, self.desc, self.udc1 = state, fila_txt, col_txt, desc, udc1
|
|
||||||
|
|
||||||
# ridistribuisci pesi griglia
|
|
||||||
for r in range(rows):
|
|
||||||
self.host.grid_rowconfigure(r, weight=1)
|
|
||||||
for c in range(cols):
|
|
||||||
self.host.grid_columnconfigure(c, weight=1)
|
|
||||||
|
|
||||||
# crea Frame+Button per cella (righe invertite: fila "a" in basso)
|
|
||||||
for r in range(rows):
|
|
||||||
row_btns = []
|
|
||||||
row_frames = []
|
|
||||||
for c in range(cols):
|
|
||||||
st = state[r][c]
|
|
||||||
code = f"{corsia}.{col_txt[r][c]}.{fila_txt[r][c]}" # PRIMA RIGA (in linea)
|
|
||||||
udc = udc1[r][c] or "" # SECONDA RIGA: barcode UDC
|
|
||||||
text = f"{code}\n{udc}"
|
|
||||||
cell = ctk.CTkFrame(self.host, corner_radius=6, border_width=0)
|
|
||||||
btn = ctk.CTkButton(
|
|
||||||
cell,
|
|
||||||
text=text,
|
|
||||||
corner_radius=6)
|
|
||||||
self._apply_cell_style(btn, st)
|
|
||||||
|
|
||||||
rr = (rows - 1) - r # capovolgi
|
|
||||||
cell.grid(row=rr, column=c, padx=1, pady=1, sticky="nsew")
|
|
||||||
btn.pack(fill="both", expand=True)
|
|
||||||
btn.configure(command=lambda rr=r, cc=c: self._open_menu(None, rr, cc))
|
|
||||||
btn.bind("<Button-3>", lambda e, rr=r, cc=c: self._open_menu(e, rr, cc))
|
|
||||||
row_btns.append(btn)
|
|
||||||
row_frames.append(cell)
|
|
||||||
self.buttons.append(row_btns)
|
|
||||||
self.btn_frames.append(row_frames)
|
|
||||||
|
|
||||||
# focus differito post-ricarica
|
|
||||||
if getattr(self, '_alive', True) and self._pending_focus and self._pending_focus[0] == corsia:
|
|
||||||
_, col, fila, _barcode = self._pending_focus
|
|
||||||
self._pending_focus = None
|
|
||||||
self._highlight_cell_by_labels(col, fila)
|
|
||||||
|
|
||||||
# ---------------- CONTEXT MENU ----------------
|
|
||||||
def _open_menu(self, event, r, c):
|
|
||||||
"""Open the context menu for a single matrix cell."""
|
|
||||||
st = self.matrix_state[r][c]
|
|
||||||
corsia = self.corsia_selezionata.get()
|
|
||||||
label = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}"
|
|
||||||
m = Menu(self, tearoff=0)
|
|
||||||
m.add_command(label="Apri dettaglio", command=lambda: self._toast(f"Dettaglio {label}"))
|
|
||||||
if st == 0:
|
|
||||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
|
||||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
|
||||||
elif st == 1:
|
|
||||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
|
||||||
m.add_command(label="Segna doppia", command=lambda: self._set_cell(r, c, 2))
|
|
||||||
else:
|
|
||||||
m.add_command(label="Segna vuoto", command=lambda: self._set_cell(r, c, 0))
|
|
||||||
m.add_command(label="Segna pieno", command=lambda: self._set_cell(r, c, 1))
|
|
||||||
m.add_separator()
|
|
||||||
m.add_command(label="Copia ubicazione", command=lambda: self._copy(label))
|
|
||||||
x = self.winfo_pointerx() if event is None else event.x_root
|
|
||||||
y = self.winfo_pointery() if event is None else event.y_root
|
|
||||||
m.tk_popup(x, y)
|
|
||||||
|
|
||||||
def _set_cell(self, r, c, val):
|
|
||||||
"""Update a cell state in memory and refresh the local statistics."""
|
|
||||||
self.matrix_state[r][c] = val
|
|
||||||
btn = self.buttons[r][c]
|
|
||||||
self._apply_cell_style(btn, val)
|
|
||||||
self._refresh_stats()
|
|
||||||
|
|
||||||
# ---------------- STATS ----------------
|
|
||||||
def _build_stats(self):
|
|
||||||
"""Create progress bars, labels and legend for occupancy statistics."""
|
|
||||||
bottom = ctk.CTkFrame(self)
|
|
||||||
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))
|
|
||||||
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))
|
|
||||||
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))
|
|
||||||
self.sel_text.grid(row=3, column=0, sticky="e")
|
|
||||||
|
|
||||||
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))
|
|
||||||
self._legend(leg, 1, "Vuota", COLOR_EMPTY)
|
|
||||||
self._legend(leg, 3, "Piena", COLOR_FULL)
|
|
||||||
self._legend(leg, 5, "Doppia UDC", COLOR_DOUBLE)
|
|
||||||
|
|
||||||
def _legend(self, parent, col, text, color):
|
|
||||||
"""Add a legend entry describing one matrix color."""
|
|
||||||
box = tk.Canvas(parent, width=18, height=12, highlightthickness=0)
|
|
||||||
box.create_rectangle(0, 0, 18, 12, fill=color, width=1, outline="#444")
|
|
||||||
box.grid(row=0, column=col)
|
|
||||||
ctk.CTkLabel(parent, text=text).grid(row=0, column=col + 1, padx=(4, 12))
|
|
||||||
|
|
||||||
# ---------------- DATA LOADING ----------------
|
|
||||||
def _load_corsie(self):
|
|
||||||
"""Load the list of aisles available for visualization."""
|
|
||||||
sql = """
|
|
||||||
WITH C AS (
|
|
||||||
SELECT DISTINCT LTRIM(RTRIM(Corsia)) AS Corsia
|
|
||||||
FROM dbo.Celle
|
|
||||||
WHERE ID <> 9999 AND (DelDataOra IS NULL) AND LTRIM(RTRIM(Corsia)) <> '7G'
|
|
||||||
)
|
|
||||||
SELECT Corsia
|
|
||||||
FROM C
|
|
||||||
ORDER BY
|
|
||||||
CASE
|
|
||||||
WHEN LEFT(Corsia,3)='MAG' AND TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) IS NOT NULL THEN 0
|
|
||||||
WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN 1
|
|
||||||
ELSE 2
|
|
||||||
END,
|
|
||||||
CASE WHEN LEFT(Corsia,3)='MAG' THEN TRY_CONVERT(int, SUBSTRING(Corsia,4,50)) END,
|
|
||||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN TRY_CONVERT(int, Corsia) END,
|
|
||||||
CASE WHEN TRY_CONVERT(int, Corsia) IS NOT NULL THEN SUBSTRING(Corsia, LEN(CAST(TRY_CONVERT(int, Corsia) AS varchar(20)))+1, 50) END,
|
|
||||||
Corsia;
|
|
||||||
"""
|
|
||||||
def _ok(res):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
|
||||||
self.lb.delete(0, tk.END)
|
|
||||||
corsie = [r[0] for r in rows]
|
|
||||||
for c in corsie:
|
|
||||||
self.lb.insert(tk.END, c)
|
|
||||||
idx = corsie.index("1A") if "1A" in corsie else (0 if corsie else -1)
|
|
||||||
if idx >= 0:
|
|
||||||
self.lb.selection_clear(0, tk.END)
|
|
||||||
self.lb.selection_set(idx)
|
|
||||||
self.lb.see(idx)
|
|
||||||
self._on_select(None)
|
|
||||||
else:
|
|
||||||
self._toast("Nessuna corsia trovata.")
|
|
||||||
self._busy.hide()
|
|
||||||
def _err(ex):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
self._busy.hide()
|
|
||||||
messagebox.showerror("Errore", f"Caricamento corsie fallito:\n{ex}")
|
|
||||||
self._async.run(self.db.query_json(sql, {}), _ok, _err, busy=self._busy, message="Carico corsie…")
|
|
||||||
|
|
||||||
def _on_select(self, _):
|
|
||||||
"""Load the selected aisle when the listbox selection changes."""
|
|
||||||
sel = self.lb.curselection()
|
|
||||||
if not sel:
|
|
||||||
return
|
|
||||||
corsia = self.lb.get(sel[0])
|
|
||||||
self.corsia_selezionata.set(corsia)
|
|
||||||
self._load_matrix(corsia)
|
|
||||||
|
|
||||||
def _select_corsia_in_listbox(self, corsia: str):
|
|
||||||
"""Select a given aisle inside the listbox if it is present."""
|
|
||||||
for i in range(self.lb.size()):
|
|
||||||
if self.lb.get(i) == corsia:
|
|
||||||
self.lb.selection_clear(0, tk.END)
|
|
||||||
self.lb.selection_set(i)
|
|
||||||
self.lb.see(i)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _load_matrix(self, corsia: str):
|
|
||||||
"""Query and render the matrix for the selected aisle."""
|
|
||||||
# nuovo token richiesta → evita che risposte vecchie spazzino la UI
|
|
||||||
self._req_counter += 1
|
|
||||||
req_id = self._req_counter
|
|
||||||
self._last_req = req_id
|
|
||||||
|
|
||||||
sql = """
|
|
||||||
WITH C AS (
|
|
||||||
SELECT
|
|
||||||
ID,
|
|
||||||
LTRIM(RTRIM(Corsia)) AS Corsia,
|
|
||||||
LTRIM(RTRIM(Fila)) AS Fila,
|
|
||||||
LTRIM(RTRIM(Colonna)) AS Colonna,
|
|
||||||
Descrizione
|
|
||||||
FROM dbo.Celle
|
|
||||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
|
||||||
AND LTRIM(RTRIM(Corsia)) <> '7G' AND LTRIM(RTRIM(Corsia)) = :corsia
|
|
||||||
),
|
|
||||||
R AS (
|
|
||||||
SELECT Fila,
|
|
||||||
DENSE_RANK() OVER (
|
|
||||||
ORDER BY CASE WHEN TRY_CONVERT(int, Fila) IS NULL THEN 1 ELSE 0 END,
|
|
||||||
TRY_CONVERT(int, Fila), Fila
|
|
||||||
) AS RowN
|
|
||||||
FROM C GROUP BY Fila
|
|
||||||
),
|
|
||||||
K AS (
|
|
||||||
SELECT Colonna,
|
|
||||||
DENSE_RANK() OVER (
|
|
||||||
ORDER BY CASE WHEN TRY_CONVERT(int, Colonna) IS NULL THEN 1 ELSE 0 END,
|
|
||||||
TRY_CONVERT(int, Colonna), Colonna
|
|
||||||
) AS ColN
|
|
||||||
FROM C GROUP BY Colonna
|
|
||||||
),
|
|
||||||
S AS (
|
|
||||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
|
||||||
FROM C AS c
|
|
||||||
LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
|
||||||
GROUP BY c.ID
|
|
||||||
),
|
|
||||||
U AS (
|
|
||||||
SELECT c.ID, MIN(g.BarcodePallet) AS FirstUDC
|
|
||||||
FROM C c
|
|
||||||
LEFT JOIN dbo.XMag_GiacenzaPallet g ON g.IDCella = c.ID
|
|
||||||
GROUP BY c.ID
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
r.RowN, k.ColN,
|
|
||||||
CASE WHEN s.n IS NULL OR s.n = 0 THEN 0
|
|
||||||
WHEN s.n = 1 THEN 1
|
|
||||||
ELSE 2 END AS Stato,
|
|
||||||
c.Descrizione,
|
|
||||||
LTRIM(RTRIM(c.Fila)) AS FilaTxt,
|
|
||||||
LTRIM(RTRIM(c.Colonna)) AS ColTxt,
|
|
||||||
U.FirstUDC
|
|
||||||
FROM C c
|
|
||||||
JOIN R r ON r.Fila = c.Fila
|
|
||||||
JOIN K k ON k.Colonna = c.Colonna
|
|
||||||
LEFT JOIN S s ON s.ID = c.ID
|
|
||||||
LEFT JOIN U ON U.ID = c.ID
|
|
||||||
ORDER BY r.RowN, k.ColN;
|
|
||||||
"""
|
|
||||||
def _ok(res):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
# ignora risposte superate
|
|
||||||
if req_id < self._last_req:
|
|
||||||
return
|
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
|
||||||
if not rows:
|
|
||||||
# mostra matrice vuota senza rimuovere il frame (evita "schermo bianco")
|
|
||||||
self._rebuild_matrix(0, 0, [], [], [], [], [], corsia)
|
|
||||||
self._refresh_stats()
|
|
||||||
self._busy.hide()
|
|
||||||
return
|
|
||||||
max_r = max_c = 0
|
|
||||||
for row in rows:
|
|
||||||
rown, coln = row[0], row[1]
|
|
||||||
if rown and coln:
|
|
||||||
max_r = max(max_r, int(rown))
|
|
||||||
max_c = max(max_c, int(coln))
|
|
||||||
mat = [[0] * max_c for _ in range(max_r)]
|
|
||||||
fila = [[""] * max_c for _ in range(max_r)]
|
|
||||||
col = [[""] * max_c for _ in range(max_r)]
|
|
||||||
desc = [[""] * max_c for _ in range(max_r)]
|
|
||||||
udc = [[""] * max_c for _ in range(max_r)]
|
|
||||||
for row in rows:
|
|
||||||
rown, coln, stato, descr, fila_txt, col_txt, first_udc = row
|
|
||||||
r = int(rown) - 1
|
|
||||||
c = int(coln) - 1
|
|
||||||
mat[r][c] = int(stato)
|
|
||||||
fila[r][c] = str(fila_txt or "")
|
|
||||||
col[r][c] = str(col_txt or "")
|
|
||||||
desc[r][c] = str(descr or f"{corsia}.{col_txt}.{fila_txt}")
|
|
||||||
udc[r][c] = str(first_udc or "")
|
|
||||||
self._rebuild_matrix(max_r, max_c, mat, fila, col, desc, udc, corsia)
|
|
||||||
self._refresh_stats()
|
|
||||||
self._busy.hide()
|
|
||||||
def _err(ex):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
if req_id < self._last_req:
|
|
||||||
return
|
|
||||||
self._busy.hide()
|
|
||||||
messagebox.showerror("Errore", f"Caricamento matrice {corsia} fallito:\n{ex}")
|
|
||||||
self._async.run(self.db.query_json(sql, {"corsia": corsia}), _ok, _err, busy=self._busy, message=f"Carico corsia {corsia}…")
|
|
||||||
|
|
||||||
# ---------------- SEARCH ----------------
|
|
||||||
def _search_udc(self):
|
|
||||||
"""Find a pallet barcode and navigate to the aisle and cell that contain it."""
|
|
||||||
barcode = (self.search_var.get() or "").strip()
|
|
||||||
if not barcode:
|
|
||||||
self._toast("Inserisci un barcode UDC da cercare.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# bump token per impedire che una vecchia _load_matrix cancelli UI
|
|
||||||
self._req_counter += 1
|
|
||||||
search_req_id = self._req_counter
|
|
||||||
self._last_req = search_req_id
|
|
||||||
|
|
||||||
sql = """
|
|
||||||
SELECT TOP (1)
|
|
||||||
RTRIM(c.Corsia) AS Corsia,
|
|
||||||
RTRIM(c.Colonna) AS Colonna,
|
|
||||||
RTRIM(c.Fila) AS Fila,
|
|
||||||
c.ID AS IDCella
|
|
||||||
FROM dbo.XMag_GiacenzaPallet g
|
|
||||||
JOIN dbo.Celle c ON c.ID = g.IDCella
|
|
||||||
WHERE g.BarcodePallet = :barcode
|
|
||||||
AND c.ID <> 9999 AND RTRIM(c.Corsia) <> '7G'
|
|
||||||
"""
|
|
||||||
def _ok(res):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
if search_req_id < self._last_req:
|
|
||||||
return
|
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
|
||||||
if not rows:
|
|
||||||
messagebox.showinfo("Ricerca", f"UDC {barcode} non trovata.", parent=self)
|
|
||||||
return
|
|
||||||
corsia, col, fila, _idc = rows[0]
|
|
||||||
corsia = str(corsia).strip(); col = str(col).strip(); fila = str(fila).strip()
|
|
||||||
self._pending_focus = (corsia, col, fila, barcode)
|
|
||||||
|
|
||||||
# sincronizza listbox e carica SEMPRE la corsia della UDC
|
|
||||||
self._select_corsia_in_listbox(corsia)
|
|
||||||
self.corsia_selezionata.set(corsia)
|
|
||||||
self._load_matrix(corsia) # highlight avverrà in _rebuild_matrix
|
|
||||||
def _err(ex):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
if search_req_id < self._last_req:
|
|
||||||
return
|
|
||||||
messagebox.showerror("Ricerca", f"Errore ricerca UDC:\n{ex}", parent=self)
|
|
||||||
|
|
||||||
self._async.run(self.db.query_json(sql, {"barcode": barcode}), _ok, _err, busy=self._busy, message="Cerco UDC…")
|
|
||||||
|
|
||||||
def _try_highlight(self, col_txt: str, fila_txt: str) -> bool:
|
|
||||||
"""Highlight a cell by its textual row and column labels."""
|
|
||||||
for r in range(len(self.col_txt)):
|
|
||||||
for c in range(len(self.col_txt[r])):
|
|
||||||
if self.col_txt[r][c] == col_txt and self.fila_txt[r][c] == fila_txt:
|
|
||||||
self._clear_highlight()
|
|
||||||
btn = self.buttons[r][c]
|
|
||||||
btn.configure(border_width=3, border_color="blue")
|
|
||||||
try:
|
|
||||||
fr = self.btn_frames[r][c]
|
|
||||||
fr.configure(border_color="blue", border_width=2)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._highlighted = (r, c)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _highlight_cell_by_labels(self, col_txt: str, fila_txt: str):
|
|
||||||
"""Show a toast when a searched cell cannot be highlighted."""
|
|
||||||
if not self._try_highlight(col_txt, fila_txt):
|
|
||||||
self._toast("Cella trovata ma non mappabile a pulsante.")
|
|
||||||
|
|
||||||
# ---------------- COMMANDS ----------------
|
|
||||||
def _refresh_current(self):
|
|
||||||
"""Reload the matrix of the currently selected aisle."""
|
|
||||||
if self.corsia_selezionata.get():
|
|
||||||
self._load_matrix(self.corsia_selezionata.get())
|
|
||||||
|
|
||||||
def _export_xlsx(self):
|
|
||||||
"""Export both matrix metadata and the rendered grid to Excel."""
|
|
||||||
if not self.matrix_state:
|
|
||||||
messagebox.showinfo("Export", "Nessuna matrice da esportare.")
|
|
||||||
return
|
|
||||||
corsia = self.corsia_selezionata.get() or "NA"
|
|
||||||
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
|
|
||||||
default = f"layout_matrice_{corsia}_{ts}.xlsx"
|
|
||||||
path = filedialog.asksaveasfilename(
|
|
||||||
title="Esporta matrice",
|
|
||||||
defaultextension=".xlsx",
|
|
||||||
initialfile=default,
|
|
||||||
filetypes=[("Excel", "*.xlsx")]
|
|
||||||
)
|
|
||||||
if not path:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.styles import PatternFill, Alignment, Font
|
|
||||||
except Exception as ex:
|
|
||||||
messagebox.showerror("Export", f"Manca openpyxl: {ex}\nInstalla con: pip install openpyxl")
|
|
||||||
return
|
|
||||||
rows = len(self.matrix_state)
|
|
||||||
cols = len(self.matrix_state[0]) if self.matrix_state else 0
|
|
||||||
wb = Workbook()
|
|
||||||
ws1 = wb.active
|
|
||||||
ws1.title = f"Dettaglio {corsia}"
|
|
||||||
ws1.append(["Corsia", "FilaIdx", "ColIdx", "Stato", "Descrizione", "FilaTxt", "ColTxt", "UDC1"])
|
|
||||||
for r in range(rows):
|
|
||||||
for c in range(cols):
|
|
||||||
st = self.matrix_state[r][c]
|
|
||||||
stato_lbl = "Vuota" if st == 0 else ("Piena" if st == 1 else "Doppia")
|
|
||||||
ws1.append([corsia, r + 1, c + 1, stato_lbl,
|
|
||||||
self.desc[r][c], self.fila_txt[r][c], self.col_txt[r][c], self.udc1[r][c]])
|
|
||||||
for cell in ws1[1]:
|
|
||||||
cell.font = Font(bold=True)
|
|
||||||
|
|
||||||
ws2 = wb.create_sheet(f"Matrice {corsia}")
|
|
||||||
fills = {
|
|
||||||
0: PatternFill("solid", fgColor="B0B0B0"),
|
|
||||||
1: PatternFill("solid", fgColor="FFA500"),
|
|
||||||
2: PatternFill("solid", fgColor="D62728"),
|
|
||||||
}
|
|
||||||
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
||||||
for r in range(rows):
|
|
||||||
for c in range(cols):
|
|
||||||
value = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}\n{self.udc1[r][c]}"
|
|
||||||
cell = ws2.cell(row=(rows - r), column=c + 1, value=value) # capovolto per avere 'a' in basso
|
|
||||||
cell.fill = fills.get(self.matrix_state[r][c], fills[0])
|
|
||||||
cell.alignment = center
|
|
||||||
try:
|
|
||||||
wb.save(path)
|
|
||||||
self._toast(f"Esportato: {path}")
|
|
||||||
except Exception as ex:
|
|
||||||
messagebox.showerror("Export", f"Salvataggio fallito:\n{ex}")
|
|
||||||
|
|
||||||
# ---------------- STATS ----------------
|
|
||||||
def _refresh_stats(self):
|
|
||||||
"""Refresh global and local occupancy statistics shown in the footer."""
|
|
||||||
# globale dal DB
|
|
||||||
sql_tot = """
|
|
||||||
WITH C AS (
|
|
||||||
SELECT ID
|
|
||||||
FROM dbo.Celle
|
|
||||||
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
|
||||||
AND LTRIM(RTRIM(Corsia)) <> '7G'
|
|
||||||
AND LTRIM(RTRIM(Fila)) IS NOT NULL
|
|
||||||
AND LTRIM(RTRIM(Colonna)) IS NOT NULL
|
|
||||||
),
|
|
||||||
S AS (
|
|
||||||
SELECT c.ID, COUNT(DISTINCT g.BarcodePallet) AS n
|
|
||||||
FROM C AS c LEFT JOIN dbo.XMag_GiacenzaPallet AS g ON g.IDCella = c.ID
|
|
||||||
GROUP BY c.ID
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
CAST(SUM(CASE WHEN s.n>0 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercPieno,
|
|
||||||
CAST(SUM(CASE WHEN s.n>1 THEN 1 ELSE 0 END) AS float)/NULLIF(COUNT(*),0) AS PercDoppie
|
|
||||||
FROM C LEFT JOIN S s ON s.ID = C.ID;
|
|
||||||
"""
|
|
||||||
def _ok(res):
|
|
||||||
if not getattr(self, '_alive', True) or not self.winfo_exists():
|
|
||||||
return
|
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
|
||||||
p_full = float(rows[0][0] or 0.0) if rows else 0.0
|
|
||||||
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)
|
|
||||||
|
|
||||||
# selezionata dalla matrice in memoria
|
|
||||||
if self.matrix_state:
|
|
||||||
tot = sum(len(r) for r in self.matrix_state)
|
|
||||||
full = sum(1 for row in self.matrix_state for v in row if v in (1, 2))
|
|
||||||
doubles = sum(1 for row in self.matrix_state for v in row if v == 2)
|
|
||||||
p_full = (full / tot) if tot else 0.0
|
|
||||||
p_dbl = (doubles / tot) if tot else 0.0
|
|
||||||
else:
|
|
||||||
p_full = p_dbl = 0.0
|
|
||||||
self._draw_bar(self.sel_canvas, p_full)
|
|
||||||
self.sel_text.configure(text=pct_text(p_full, p_dbl))
|
|
||||||
|
|
||||||
def _draw_bar(self, cv: tk.Canvas, p_full: float):
|
|
||||||
"""Draw a horizontal occupancy bar on the given canvas."""
|
|
||||||
cv.delete("all")
|
|
||||||
w = max(300, cv.winfo_width() or 600)
|
|
||||||
h = 18
|
|
||||||
fw = int(w * max(0.0, min(1.0, p_full)))
|
|
||||||
cv.create_rectangle(2, 2, 2 + fw, 2 + h, fill="#D62728", width=0)
|
|
||||||
cv.create_rectangle(2 + fw, 2, 2 + w, 2 + h, fill="#2CA02C", width=0)
|
|
||||||
cv.create_rectangle(2, 2, 2 + w, 2 + h, outline="#555", width=1)
|
|
||||||
|
|
||||||
# ---------------- UTIL ----------------
|
|
||||||
def _toast(self, msg, ms=1400):
|
|
||||||
"""Show a transient status message at the bottom of the window."""
|
|
||||||
if not hasattr(self, "_status"):
|
|
||||||
self._status = ctk.CTkLabel(self, anchor="w")
|
|
||||||
self._status.grid(row=3, column=0, sticky="ew")
|
|
||||||
self._status.configure(text=msg)
|
|
||||||
self.after(ms, lambda: self._status.configure(text=""))
|
|
||||||
|
|
||||||
def _copy(self, txt: str):
|
|
||||||
"""Copy a string to the clipboard and inform the user."""
|
|
||||||
self.clipboard_clear()
|
|
||||||
self.clipboard_append(txt)
|
|
||||||
self._toast(f"Copiato: {txt}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def destroy(self):
|
|
||||||
"""Mark the window as closed and release dynamic widgets safely."""
|
|
||||||
# evita nuovi refresh/async dopo destroy
|
|
||||||
self._alive = False
|
|
||||||
# cancella eventuali timer
|
|
||||||
try:
|
|
||||||
if self._stats_after_id is not None:
|
|
||||||
self.after_cancel(self._stats_after_id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# pulizia UI leggera
|
|
||||||
try:
|
|
||||||
for w in list(self.host.winfo_children()):
|
|
||||||
w.destroy()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
super().destroy()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def open_layout_window(parent, db_app):
|
|
||||||
"""Open the layout window as a singleton-like child of ``parent``."""
|
|
||||||
key = "_layout_window_singleton"
|
|
||||||
ex = getattr(parent, key, None)
|
|
||||||
if ex and ex.winfo_exists():
|
|
||||||
try:
|
|
||||||
ex.lift()
|
|
||||||
ex.focus_force()
|
|
||||||
return ex
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
w = LayoutWindow(parent, db_app)
|
|
||||||
setattr(parent, key, w)
|
|
||||||
return w
|
|
||||||
229
locale.json
Normal file
229
locale.json
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"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.history_udc": "Storico movimenti UDC",
|
||||||
|
"launcher.pickinglist": "Gestione Picking List",
|
||||||
|
"launcher.history_pickinglist": "Storico 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...",
|
||||||
|
"history.udc.title": "Storico movimenti UDC",
|
||||||
|
"history.udc.button.search": "Cerca",
|
||||||
|
"history.udc.busy": "Carico storico UDC...",
|
||||||
|
"history.udc.msg.title": "Storico UDC",
|
||||||
|
"history.udc.msg.empty": "Nessun movimento trovato.",
|
||||||
|
"history.udc.msg.error": "Errore ricerca:\n{error}",
|
||||||
|
"history.picking.title": "Storico Picking List",
|
||||||
|
"history.picking.button.reload": "Ricarica",
|
||||||
|
"history.picking.detail_title": "Dettaglio contenuto",
|
||||||
|
"history.picking.busy.master": "Carico storico picking list...",
|
||||||
|
"history.picking.busy.detail": "Carico dettaglio picking list...",
|
||||||
|
"history.picking.msg.title": "Storico Picking List",
|
||||||
|
"history.picking.msg.load_error": "Errore caricamento:\n{error}",
|
||||||
|
"history.picking.msg.detail_error": "Errore dettaglio:\n{error}",
|
||||||
|
"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.history_udc": "UDC Movement History",
|
||||||
|
"launcher.pickinglist": "Picking List Management",
|
||||||
|
"launcher.history_pickinglist": "Picking List History",
|
||||||
|
"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...",
|
||||||
|
"history.udc.title": "UDC Movement History",
|
||||||
|
"history.udc.button.search": "Search",
|
||||||
|
"history.udc.busy": "Loading UDC history...",
|
||||||
|
"history.udc.msg.title": "UDC History",
|
||||||
|
"history.udc.msg.empty": "No movement found.",
|
||||||
|
"history.udc.msg.error": "Search failed:\n{error}",
|
||||||
|
"history.picking.title": "Picking List History",
|
||||||
|
"history.picking.button.reload": "Reload",
|
||||||
|
"history.picking.detail_title": "Content detail",
|
||||||
|
"history.picking.busy.master": "Loading picking-list history...",
|
||||||
|
"history.picking.busy.detail": "Loading picking-list detail...",
|
||||||
|
"history.picking.msg.title": "Picking List History",
|
||||||
|
"history.picking.msg.load_error": "Load failed:\n{error}",
|
||||||
|
"history.picking.msg.detail_error": "Detail load failed:\n{error}",
|
||||||
|
"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)
|
||||||
308
login_window.py
Normal file
308
login_window.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"""Application login dialog backed by the ``Operatori`` table."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox, ttk
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from audit_log import log_session_event
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
SQL_LOGIN = """
|
||||||
|
SELECT TOP (1)
|
||||||
|
ID,
|
||||||
|
Login,
|
||||||
|
Nominativo,
|
||||||
|
Privilegio,
|
||||||
|
CodiceUnita
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE LTRIM(RTRIM(Login)) = :login
|
||||||
|
AND LTRIM(RTRIM([Password])) = :password
|
||||||
|
ORDER BY ID;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_to_dicts(res: Any) -> list[dict[str, Any]]:
|
||||||
|
"""Normalize DB responses into a list of row dictionaries."""
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
return []
|
||||||
|
if isinstance(res, list):
|
||||||
|
return [row for row in res if isinstance(row, dict)]
|
||||||
|
if isinstance(res, dict):
|
||||||
|
rows = res.get("rows") or res.get("data") or res.get("records") or []
|
||||||
|
if rows and isinstance(rows[0], dict):
|
||||||
|
return rows
|
||||||
|
cols = res.get("columns") or []
|
||||||
|
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
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class LoginWindow(tk.Toplevel):
|
||||||
|
"""Small modal dialog used to authenticate one warehouse operator."""
|
||||||
|
|
||||||
|
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._show_ready_after_id: str | None = None
|
||||||
|
self._clear_topmost_after_id: str | None = None
|
||||||
|
|
||||||
|
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", "165x155+0+0")))
|
||||||
|
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.login_var = tk.StringVar()
|
||||||
|
self.password_var = tk.StringVar()
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self.update_idletasks()
|
||||||
|
self.grab_set()
|
||||||
|
self.deiconify()
|
||||||
|
self.lift()
|
||||||
|
self.attributes("-topmost", True)
|
||||||
|
self._show_ready_after_id = self.after(50, self._show_ready)
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
"""Build the compact operator login form."""
|
||||||
|
|
||||||
|
body = ttk.Frame(self, padding=8 if self.compact else 8)
|
||||||
|
body.pack(fill="both", expand=True)
|
||||||
|
body.columnconfigure(1, weight=0)
|
||||||
|
|
||||||
|
row_offset = 0
|
||||||
|
|
||||||
|
ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=4)
|
||||||
|
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="w", pady=4)
|
||||||
|
|
||||||
|
ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=4)
|
||||||
|
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="w", pady=4)
|
||||||
|
|
||||||
|
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.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555")
|
||||||
|
self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 2))
|
||||||
|
|
||||||
|
actions = ttk.Frame(body)
|
||||||
|
actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(6, 0))
|
||||||
|
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=1, column=0, sticky="ew", pady=(4, 0))
|
||||||
|
self._login_button = ttk.Button(
|
||||||
|
actions,
|
||||||
|
text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"),
|
||||||
|
command=self._on_login,
|
||||||
|
)
|
||||||
|
self._login_button.grid(row=0, column=0, sticky="ew")
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
state = "disabled" if busy else "normal"
|
||||||
|
try:
|
||||||
|
self.login_entry.configure(state=state)
|
||||||
|
self.password_entry.configure(state=state)
|
||||||
|
if self._login_button is not None:
|
||||||
|
self._login_button.configure(state=state)
|
||||||
|
if self._cancel_button is not None:
|
||||||
|
self._cancel_button.configure(state=state)
|
||||||
|
self.configure(cursor="watch" if busy else "")
|
||||||
|
self._status_var.set(message)
|
||||||
|
self.update_idletasks()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _focus_login(self) -> None:
|
||||||
|
"""Focus the login entry as soon as the modal becomes visible."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.login_entry.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _show_ready(self) -> None:
|
||||||
|
"""Make the login visible and ready even when the bootstrap root is hidden."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.attributes("-topmost", True)
|
||||||
|
self.deiconify()
|
||||||
|
self.lift()
|
||||||
|
self.focus_force()
|
||||||
|
self._focus_login()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._clear_topmost_after_id = self.after(250, lambda: self.attributes("-topmost", False))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_login(self) -> None:
|
||||||
|
"""Validate credentials against the Operatori table."""
|
||||||
|
|
||||||
|
login = str(self.login_var.get() or "").strip()
|
||||||
|
password = str(self.password_var.get() or "").strip()
|
||||||
|
if not login or not password:
|
||||||
|
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, loc_text("login.status.checking", catalog=self._locale_catalog, default="Verifico credenziali..."))
|
||||||
|
|
||||||
|
def _ok(res: Any) -> None:
|
||||||
|
rows = _rows_to_dicts(res)
|
||||||
|
self._set_busy(False, "")
|
||||||
|
if not rows:
|
||||||
|
log_session_event(
|
||||||
|
None,
|
||||||
|
action="login.failed",
|
||||||
|
outcome="denied",
|
||||||
|
details={"login": login},
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
row = rows[0]
|
||||||
|
self.result_session = create_user_session(
|
||||||
|
operator_id=int(row.get("ID") or 0),
|
||||||
|
login=str(row.get("Login") or login).strip(),
|
||||||
|
nominativo=str(row.get("Nominativo") or "").strip(),
|
||||||
|
privilegio=int(row["Privilegio"]) if row.get("Privilegio") is not None else None,
|
||||||
|
codice_unita=str(row.get("CodiceUnita") or "").strip(),
|
||||||
|
)
|
||||||
|
log_session_event(
|
||||||
|
self.result_session,
|
||||||
|
action="login.success",
|
||||||
|
outcome="ok",
|
||||||
|
details={"display_name": self.result_session.display_name},
|
||||||
|
)
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def _err(ex: Exception) -> None:
|
||||||
|
self._set_busy(False, "")
|
||||||
|
log_session_event(
|
||||||
|
None,
|
||||||
|
action="login.error",
|
||||||
|
outcome="error",
|
||||||
|
details={"login": login, "error": str(ex)},
|
||||||
|
)
|
||||||
|
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),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_cancel(self) -> None:
|
||||||
|
"""Abort the login flow and close the modal."""
|
||||||
|
|
||||||
|
log_session_event(None, action="login.cancel", outcome="cancelled")
|
||||||
|
self.result_session = None
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def _close(self) -> None:
|
||||||
|
"""Release the modal grab and destroy the login window."""
|
||||||
|
|
||||||
|
if self._show_ready_after_id is not None:
|
||||||
|
try:
|
||||||
|
self.after_cancel(self._show_ready_after_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._show_ready_after_id = None
|
||||||
|
if self._clear_topmost_after_id is not None:
|
||||||
|
try:
|
||||||
|
self.after_cancel(self._clear_topmost_after_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._clear_topmost_after_id = None
|
||||||
|
try:
|
||||||
|
self.grab_release()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_login(parent: tk.Misc, db_client) -> UserSession | None:
|
||||||
|
"""Open the login modal and return the authenticated user session, if any."""
|
||||||
|
|
||||||
|
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
|
||||||
698
main.py
698
main.py
@@ -6,55 +6,54 @@ project.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import ctypes
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
import time
|
||||||
|
|
||||||
|
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||||
|
|
||||||
|
ensure_stdio("warehouse_main")
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
from async_loop_singleton import get_global_loop
|
from async_loop_singleton import get_global_loop, stop_global_loop
|
||||||
from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn
|
from async_msssql_query import AsyncMSSQLClient
|
||||||
from layout_window import open_layout_window
|
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 reset_corsie import open_reset_corsie_window
|
||||||
from search_pallets import open_search_window
|
from search_pallets import open_search_window
|
||||||
from view_celle_multiple import open_celle_multiple_window
|
from storico_pickinglist import open_storico_pickinglist_window
|
||||||
|
from storico_udc import open_storico_udc_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_below_parent_later,
|
||||||
|
place_window_fullsize_below_parent_later,
|
||||||
|
)
|
||||||
|
|
||||||
# Try factory, else frame, else app (senza passare conn_str all'App)
|
# Development shortcut: skip the login dialog and boot directly as MAG1.
|
||||||
try:
|
# Set to False when you want to restore normal authentication.
|
||||||
from gestione_pickinglist import create_frame as create_pickinglist_frame
|
BYPASS_LOGIN = False
|
||||||
except Exception:
|
BYPASS_LOGIN_USER = {
|
||||||
try:
|
"operator_id": 4,
|
||||||
from gestione_pickinglist import GestionePickingListFrame as _PLFrame
|
"login": "MAG1",
|
||||||
|
"nominativo": "MAG1",
|
||||||
|
"privilegio": 3,
|
||||||
|
"codice_unita": "U1",
|
||||||
|
}
|
||||||
|
|
||||||
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
|
# Create one global loop for database work. Tk must keep the main thread clean;
|
||||||
"""Build the picking list UI using the frame-based fallback."""
|
# callers schedule async jobs on this loop explicitly.
|
||||||
ctk.set_appearance_mode("light")
|
|
||||||
ctk.set_default_color_theme("green")
|
|
||||||
return _PLFrame(parent, db_client=db_client, conn_str=conn_str)
|
|
||||||
except Exception:
|
|
||||||
from gestione_pickinglist import GestionePickingListApp as _PLApp
|
|
||||||
|
|
||||||
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
|
|
||||||
"""Fallback used only by legacy app-style picking list implementations."""
|
|
||||||
app = _PLApp()
|
|
||||||
app.mainloop()
|
|
||||||
return tk.Frame(parent)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Config ----
|
|
||||||
SERVER = r"mde3\gesterp"
|
|
||||||
DBNAME = "Mediseawall"
|
|
||||||
USER = "sa"
|
|
||||||
PASSWORD = "1Password1"
|
|
||||||
|
|
||||||
if sys.platform.startswith("win"):
|
|
||||||
try:
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create one global loop and make it the default everywhere.
|
|
||||||
_loop = get_global_loop()
|
_loop = get_global_loop()
|
||||||
asyncio.set_event_loop(_loop)
|
|
||||||
|
|
||||||
|
|
||||||
def _noop(*args, **kwargs):
|
def _noop(*args, **kwargs):
|
||||||
@@ -67,114 +66,543 @@ if not hasattr(tk.Toplevel, "block_update_dimensions_event"):
|
|||||||
if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
|
if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
|
||||||
tk.Toplevel.unblock_update_dimensions_event = _noop # type: ignore[attr-defined]
|
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 | None = None
|
||||||
db_app = AsyncMSSQLClient(dsn_app)
|
_APP_MUTEX = None
|
||||||
|
_APP_MUTEX_NAME = "Local\\WarehousePythonAppSingleton"
|
||||||
|
|
||||||
|
|
||||||
def open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient):
|
def _dispose_db_client() -> None:
|
||||||
"""Open the picking list window while minimizing initial flicker."""
|
"""Best-effort disposal of the shared DB client."""
|
||||||
win = ctk.CTkToplevel(parent)
|
|
||||||
win.title("Gestione Picking List")
|
|
||||||
win.geometry("1200x700+0+100")
|
|
||||||
win.minsize(1000, 560)
|
|
||||||
|
|
||||||
# Keep the toplevel hidden while its content is being created.
|
global db_app
|
||||||
try:
|
if db_app is None:
|
||||||
win.withdraw()
|
return
|
||||||
win.attributes("-alpha", 0.0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
frame = create_pickinglist_frame(win, db_client=db_client)
|
|
||||||
try:
|
|
||||||
frame.pack(fill="both", expand=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Show the window only when the layout is ready.
|
|
||||||
try:
|
|
||||||
win.update_idletasks()
|
|
||||||
try:
|
|
||||||
win.transient(parent)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
win.deiconify()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
win.lift()
|
|
||||||
try:
|
|
||||||
win.focus_force()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
win.attributes("-alpha", 1.0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
win.bind("<Escape>", lambda e: win.destroy())
|
|
||||||
win.protocol("WM_DELETE_WINDOW", win.destroy)
|
|
||||||
return win
|
|
||||||
|
|
||||||
|
|
||||||
class Launcher(ctk.CTk):
|
|
||||||
"""Main launcher window that exposes the available warehouse tools."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Create the launcher toolbar and wire every button to a feature window."""
|
|
||||||
super().__init__()
|
|
||||||
self.title("Warehouse 1.0.0")
|
|
||||||
self.geometry("1200x70+0+0")
|
|
||||||
|
|
||||||
wrap = ctk.CTkFrame(self)
|
|
||||||
wrap.pack(pady=10, fill="x")
|
|
||||||
|
|
||||||
ctk.CTkButton(
|
|
||||||
wrap,
|
|
||||||
text="Gestione Corsie",
|
|
||||||
command=lambda: open_reset_corsie_window(self, db_app),
|
|
||||||
).grid(row=0, column=0, padx=6, pady=6, sticky="ew")
|
|
||||||
ctk.CTkButton(
|
|
||||||
wrap,
|
|
||||||
text="Gestione Layout",
|
|
||||||
command=lambda: open_layout_window(self, db_app),
|
|
||||||
).grid(row=0, column=1, padx=6, pady=6, sticky="ew")
|
|
||||||
ctk.CTkButton(
|
|
||||||
wrap,
|
|
||||||
text="UDC Fantasma",
|
|
||||||
command=lambda: open_celle_multiple_window(self, db_app),
|
|
||||||
).grid(row=0, column=2, padx=6, pady=6, sticky="ew")
|
|
||||||
ctk.CTkButton(
|
|
||||||
wrap,
|
|
||||||
text="Ricerca UDC",
|
|
||||||
command=lambda: open_search_window(self, db_app),
|
|
||||||
).grid(row=0, column=3, padx=6, pady=6, sticky="ew")
|
|
||||||
ctk.CTkButton(
|
|
||||||
wrap,
|
|
||||||
text="Gestione Picking List",
|
|
||||||
command=lambda: open_pickinglist_window(self, db_app),
|
|
||||||
).grid(row=0, column=4, padx=6, pady=6, sticky="ew")
|
|
||||||
|
|
||||||
for i in range(5):
|
|
||||||
wrap.grid_columnconfigure(i, weight=1)
|
|
||||||
|
|
||||||
def _on_close():
|
|
||||||
"""Dispose shared resources before closing the launcher."""
|
|
||||||
try:
|
try:
|
||||||
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
||||||
try:
|
try:
|
||||||
fut.result(timeout=2)
|
fut.result(timeout=2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
db_app = None
|
||||||
|
|
||||||
|
|
||||||
|
def _destroy_tk_root(root: tk.Misc | None) -> None:
|
||||||
|
"""Destroy a hidden bootstrap root without leaking a Tk interpreter."""
|
||||||
|
|
||||||
|
if root is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
root.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _shutdown_runtime(*, bootstrap: tk.Misc | None = None, dispose_db: bool = True) -> None:
|
||||||
|
"""Release temporary Tk resources, DB client and background loop."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
_destroy_tk_root(bootstrap)
|
||||||
|
finally:
|
||||||
|
if dispose_db:
|
||||||
|
_dispose_db_client()
|
||||||
|
try:
|
||||||
|
stop_global_loop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _acquire_single_instance_mutex() -> bool:
|
||||||
|
"""Return ``True`` only for the first running instance of the application."""
|
||||||
|
|
||||||
|
global _APP_MUTEX
|
||||||
|
if not sys.platform.startswith("win"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
mutex = kernel32.CreateMutexW(None, False, _APP_MUTEX_NAME)
|
||||||
|
if not mutex:
|
||||||
|
return True
|
||||||
|
|
||||||
|
last_error = kernel32.GetLastError()
|
||||||
|
_APP_MUTEX = mutex
|
||||||
|
ERROR_ALREADY_EXISTS = 183
|
||||||
|
return last_error != ERROR_ALREADY_EXISTS
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bypass_session() -> UserSession:
|
||||||
|
"""Create the development session used when authentication is bypassed."""
|
||||||
|
|
||||||
|
return create_user_session(
|
||||||
|
operator_id=int(BYPASS_LOGIN_USER["operator_id"]),
|
||||||
|
login=str(BYPASS_LOGIN_USER["login"]),
|
||||||
|
nominativo=str(BYPASS_LOGIN_USER["nominativo"]),
|
||||||
|
privilegio=int(BYPASS_LOGIN_USER["privilegio"]),
|
||||||
|
codice_unita=str(BYPASS_LOGIN_USER["codice_unita"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Launcher(ctk.CTk):
|
||||||
|
"""Main launcher window that exposes the available warehouse tools."""
|
||||||
|
|
||||||
|
_WINDOW_ORDER = [
|
||||||
|
"reset_corsie",
|
||||||
|
"layout",
|
||||||
|
"multi_udc",
|
||||||
|
"search",
|
||||||
|
"storico_udc",
|
||||||
|
"pickinglist",
|
||||||
|
"storico_pickinglist",
|
||||||
|
]
|
||||||
|
|
||||||
|
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._exit_icon = self._make_exit_icon(
|
||||||
|
color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d"))
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
outer_padx = int(theme_value(self._theme, "outer_padx", 0))
|
||||||
|
info_padx = int(theme_value(self._theme, "info_padx", 6))
|
||||||
|
info_pady = theme_value(self._theme, "info_pady", [4, 2])
|
||||||
|
button_padx = int(theme_value(self._theme, "button_padx", 6))
|
||||||
|
button_pady = int(theme_value(self._theme, "button_pady", 6))
|
||||||
|
max_buttons_per_row = max(1, int(theme_value(self._theme, "max_buttons_per_row", 7)))
|
||||||
|
|
||||||
|
wrap = ctk.CTkFrame(self)
|
||||||
|
wrap.pack(padx=outer_padx, pady=outer_pady, fill="x")
|
||||||
|
|
||||||
|
actions = [
|
||||||
|
(
|
||||||
|
"reset_corsie",
|
||||||
|
loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
|
||||||
|
"launcher.open_reset_corsie",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"reset_corsie",
|
||||||
|
lambda: open_reset_corsie_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"layout",
|
||||||
|
loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
|
||||||
|
"launcher.open_layout",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"layout",
|
||||||
|
lambda: open_layout_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"multi_udc",
|
||||||
|
loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
|
||||||
|
"launcher.open_multi_udc",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"multi_udc",
|
||||||
|
lambda: open_celle_multiple_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"search",
|
||||||
|
loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
|
||||||
|
"launcher.open_search",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"search",
|
||||||
|
lambda: open_search_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"storico_udc",
|
||||||
|
loc_text("launcher.history_udc", catalog=self._locale_catalog, default="Storico movimenti UDC"),
|
||||||
|
"launcher.open_history_udc",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"storico_udc",
|
||||||
|
lambda: open_storico_udc_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pickinglist",
|
||||||
|
loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
|
||||||
|
"launcher.open_pickinglist",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"pickinglist",
|
||||||
|
lambda: open_pickinglist_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"storico_pickinglist",
|
||||||
|
loc_text("launcher.history_pickinglist", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||||
|
"launcher.open_history_pickinglist",
|
||||||
|
lambda: self._open_or_focus_child_window(
|
||||||
|
"storico_pickinglist",
|
||||||
|
lambda: open_storico_pickinglist_window(self, self.db_client, session=self.session),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"arrange",
|
||||||
|
loc_text("launcher.arrange", catalog=self._locale_catalog, default="Ridisponi finestre"),
|
||||||
|
"launcher.arrange_windows",
|
||||||
|
self._cascade_open_windows,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"exit",
|
||||||
|
loc_text("launcher.exit", catalog=self._locale_catalog, default="Esci"),
|
||||||
|
"launcher.exit",
|
||||||
|
self._shutdown,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
used_columns = max(1, min(len(actions), max_buttons_per_row))
|
||||||
|
|
||||||
|
info = ctk.CTkLabel(
|
||||||
|
wrap,
|
||||||
|
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")),
|
||||||
|
)
|
||||||
|
info.grid(row=0, column=0, columnspan=used_columns, padx=info_padx, pady=tuple(info_pady), sticky="ew")
|
||||||
|
|
||||||
|
for idx, (_key, label, permission, callback) in enumerate(actions):
|
||||||
|
row = 1 + (idx // max_buttons_per_row)
|
||||||
|
column = idx % max_buttons_per_row
|
||||||
|
text = label
|
||||||
|
button_options = {}
|
||||||
|
if _key == "exit":
|
||||||
|
row = 2
|
||||||
|
column = max_buttons_per_row - 1
|
||||||
|
text = label
|
||||||
|
button_options = {
|
||||||
|
"image": self._exit_icon,
|
||||||
|
"compound": "left",
|
||||||
|
}
|
||||||
|
button = ctk.CTkButton(
|
||||||
|
wrap,
|
||||||
|
text=text,
|
||||||
|
font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
|
||||||
|
state="normal" if self.session.can(permission) else "disabled",
|
||||||
|
command=callback,
|
||||||
|
**button_options,
|
||||||
|
)
|
||||||
|
button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
|
||||||
|
tip = tooltip_text(permission, catalog=self._tooltip_catalog)
|
||||||
|
if tip:
|
||||||
|
WidgetToolTip(button, tip)
|
||||||
|
|
||||||
|
for i in range(max_buttons_per_row):
|
||||||
|
wrap.grid_columnconfigure(i, weight=1)
|
||||||
|
|
||||||
|
self.update_idletasks()
|
||||||
|
self._apply_dynamic_geometry()
|
||||||
|
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self._shutdown)
|
||||||
|
try:
|
||||||
|
self.lift()
|
||||||
|
self.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _make_exit_icon(self, *, color: str) -> tk.PhotoImage:
|
||||||
|
"""Create a small red X icon without adding image assets to the project."""
|
||||||
|
|
||||||
|
size = 14
|
||||||
|
image = tk.PhotoImage(width=size, height=size)
|
||||||
|
for offset in range(3, 11):
|
||||||
|
image.put(color, (offset, offset))
|
||||||
|
image.put(color, (offset + 1, offset))
|
||||||
|
image.put(color, (offset, size - 1 - offset))
|
||||||
|
image.put(color, (offset + 1, size - 1 - offset))
|
||||||
|
return image
|
||||||
|
|
||||||
|
def _apply_dynamic_geometry(self) -> None:
|
||||||
|
"""Size the launcher around its current content and keep it docked at the top."""
|
||||||
|
|
||||||
|
top_x = int(theme_value(self._theme, "window_top_x", 0))
|
||||||
|
top_y = int(theme_value(self._theme, "window_top_y", 0))
|
||||||
|
min_width = int(theme_value(self._theme, "window_min_width", 960))
|
||||||
|
target_width = int(theme_value(self._theme, "window_width", 1280))
|
||||||
|
|
||||||
|
requested_width = max(min_width, self.winfo_reqwidth())
|
||||||
|
width = max(min_width, target_width, requested_width)
|
||||||
|
height = max(80, self.winfo_reqheight())
|
||||||
|
self.geometry(f"{width}x{height}+{top_x}+{top_y}")
|
||||||
|
|
||||||
|
def _open_child_window(self, key: str, window: tk.Misc | None) -> None:
|
||||||
|
"""Track child windows opened from the launcher."""
|
||||||
|
|
||||||
|
if window is None:
|
||||||
|
return
|
||||||
|
self._child_windows = [w for w in self._child_windows if getattr(w, "winfo_exists", lambda: False)()]
|
||||||
|
if window not in self._child_windows:
|
||||||
|
self._child_windows.append(window)
|
||||||
|
self._child_windows_by_key = {
|
||||||
|
child_key: child
|
||||||
|
for child_key, child in self._child_windows_by_key.items()
|
||||||
|
if getattr(child, "winfo_exists", lambda: False)()
|
||||||
|
}
|
||||||
|
self._child_windows_by_key[key] = window
|
||||||
|
try:
|
||||||
|
window.bind(
|
||||||
|
"<Destroy>",
|
||||||
|
lambda event, win=window, win_key=key: self._forget_child_window(win_key, win, event.widget),
|
||||||
|
add="+",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
window.bind(
|
||||||
|
"<Activate>",
|
||||||
|
lambda event, win=window, win_key=key: self._on_child_activate(win_key, win, event.widget),
|
||||||
|
add="+",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
window.bind(
|
||||||
|
"<FocusIn>",
|
||||||
|
lambda event, win=window, win_key=key: self._on_child_activate(win_key, win, event.widget),
|
||||||
|
add="+",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _open_or_focus_child_window(self, key: str, factory) -> None:
|
||||||
|
"""Open one child per launcher key, or focus the existing window."""
|
||||||
|
|
||||||
|
self._child_windows_by_key = {
|
||||||
|
child_key: child
|
||||||
|
for child_key, child in self._child_windows_by_key.items()
|
||||||
|
if getattr(child, "winfo_exists", lambda: False)()
|
||||||
|
}
|
||||||
|
existing = self._child_windows_by_key.get(key)
|
||||||
|
if existing is not None and getattr(existing, "winfo_exists", lambda: False)():
|
||||||
|
try:
|
||||||
|
if hasattr(existing, "state") and existing.state() == "iconic":
|
||||||
|
existing.deiconify()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
existing.lift()
|
||||||
|
existing.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
place_window_below_parent_later(self, existing)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
self._open_child_window(key, factory())
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
if event_widget is not None and event_widget is not window:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
tracked = self._child_windows_by_key.get(key)
|
||||||
|
if tracked is window:
|
||||||
|
self._child_windows_by_key.pop(key, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self._child_windows = [w for w in self._child_windows if w is not window and getattr(w, "winfo_exists", lambda: False)()]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _widget_belongs_to_window(self, window: tk.Misc, widget: tk.Misc | None) -> bool:
|
||||||
|
"""Return True when an event widget is the toplevel itself or one of its descendants."""
|
||||||
|
|
||||||
|
if widget is None:
|
||||||
|
return True
|
||||||
|
if widget is window:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
current = widget
|
||||||
|
while current is not None:
|
||||||
|
if current is window:
|
||||||
|
return True
|
||||||
|
parent_name = current.winfo_parent()
|
||||||
|
if not parent_name:
|
||||||
|
break
|
||||||
|
current = current.nametowidget(parent_name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_child_activate(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None:
|
||||||
|
"""Restore an activated child to the primary slot below the launcher."""
|
||||||
|
|
||||||
|
if self._is_cascading:
|
||||||
|
return
|
||||||
|
if time.monotonic() < self._restore_suppressed_until:
|
||||||
|
return
|
||||||
|
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)():
|
||||||
|
return
|
||||||
|
if key in self._focus_restore_pending:
|
||||||
|
return
|
||||||
|
self._focus_restore_pending.add(key)
|
||||||
|
|
||||||
|
def _restore() -> None:
|
||||||
|
try:
|
||||||
|
tracked_now = self._child_windows_by_key.get(key)
|
||||||
|
if tracked_now is window and getattr(window, "winfo_exists", lambda: False)():
|
||||||
|
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))
|
||||||
|
except Exception:
|
||||||
|
self._focus_restore_pending.discard(key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.after(0, _restore)
|
||||||
|
except Exception:
|
||||||
|
self._focus_restore_pending.discard(key)
|
||||||
|
|
||||||
|
def _cascade_open_windows(self) -> None:
|
||||||
|
"""Cascade all open child windows following launcher button order."""
|
||||||
|
|
||||||
|
self._child_windows = [w for w in self._child_windows if getattr(w, "winfo_exists", lambda: False)()]
|
||||||
|
self._child_windows_by_key = {
|
||||||
|
key: window
|
||||||
|
for key, window in self._child_windows_by_key.items()
|
||||||
|
if getattr(window, "winfo_exists", lambda: False)()
|
||||||
|
}
|
||||||
|
ordered_windows = [
|
||||||
|
self._child_windows_by_key[key]
|
||||||
|
for key in self._WINDOW_ORDER
|
||||||
|
if key in self._child_windows_by_key
|
||||||
|
]
|
||||||
|
self._is_cascading = True
|
||||||
|
self._focus_restore_pending.clear()
|
||||||
|
self._restore_suppressed_until = time.monotonic() + 1.2
|
||||||
|
cascade_children_below_parent(
|
||||||
|
self,
|
||||||
|
ordered_windows,
|
||||||
|
x_offset_step=int(theme_value(self._theme, "cascade_x_offset", 28)),
|
||||||
|
y_offset_step=int(theme_value(self._theme, "cascade_y_offset", 28)),
|
||||||
|
margin_left=int(theme_value(self._theme, "cascade_margin_left", 0)),
|
||||||
|
margin_top=int(theme_value(self._theme, "cascade_margin_top", 0)),
|
||||||
|
)
|
||||||
|
def _finish_cascade() -> None:
|
||||||
|
self._is_cascading = False
|
||||||
|
self._restore_suppressed_until = 0.0
|
||||||
|
try:
|
||||||
|
self.lift()
|
||||||
|
self.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.after(250, _finish_cascade)
|
||||||
|
except Exception:
|
||||||
|
self._is_cascading = False
|
||||||
|
|
||||||
|
def _shutdown(self) -> None:
|
||||||
|
"""Dispose session and shared DB resources before closing the launcher."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.session is not None:
|
||||||
|
log_session_event(self.session, action="logout", outcome="ok")
|
||||||
|
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:
|
finally:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
self.protocol("WM_DELETE_WINDOW", _on_close)
|
|
||||||
|
def run_app() -> int:
|
||||||
|
"""Run the backoffice application entry point."""
|
||||||
|
|
||||||
|
ctk.set_appearance_mode("light")
|
||||||
|
ctk.set_default_color_theme("green")
|
||||||
|
if not _acquire_single_instance_mutex():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
messagebox.showwarning(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
_destroy_tk_root(root)
|
||||||
|
try:
|
||||||
|
stop_global_loop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
db_cfg = ensure_db_config(_loop)
|
||||||
|
if db_cfg is None:
|
||||||
|
try:
|
||||||
|
stop_global_loop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))
|
||||||
|
|
||||||
|
if BYPASS_LOGIN:
|
||||||
|
session = _build_bypass_session()
|
||||||
|
log_session_event(
|
||||||
|
session,
|
||||||
|
action="login.bypass",
|
||||||
|
outcome="ok",
|
||||||
|
details={"login": session.login},
|
||||||
|
)
|
||||||
|
bootstrap = None
|
||||||
|
else:
|
||||||
|
bootstrap = tk.Tk()
|
||||||
|
bootstrap.geometry("1x1+0+0")
|
||||||
|
bootstrap.overrideredirect(True)
|
||||||
|
bootstrap.attributes("-alpha", 0.0)
|
||||||
|
bootstrap.deiconify()
|
||||||
|
bootstrap.update_idletasks()
|
||||||
|
session = prompt_login(bootstrap, db_app)
|
||||||
|
|
||||||
|
if session is None:
|
||||||
|
_shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
_destroy_tk_root(bootstrap)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Launcher(session, db_app).mainloop()
|
||||||
|
finally:
|
||||||
|
_shutdown_runtime(bootstrap=None, dispose_db=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ctk.set_appearance_mode("light")
|
raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))
|
||||||
ctk.set_default_color_theme("green")
|
|
||||||
Launcher().mainloop()
|
|
||||||
|
|||||||
343
patch_sp_xExePackingListPallet.sql
Normal file
343
patch_sp_xExePackingListPallet.sql
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Patch prenotazione Picking List a livello documento
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
- rendere esclusiva la prenotazione di una sola picking list
|
||||||
|
- mantenere la stored con lo stesso nome:
|
||||||
|
dbo.sp_xExePackingListPallet
|
||||||
|
- introdurre una sorgente di verità per documento prenotato:
|
||||||
|
dbo.PickingListReservation
|
||||||
|
- far leggere XMag_ViewPackingList.IDStato da tale stato
|
||||||
|
e non più soltanto da Celle.IDStato
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.PickingListReservation', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[PickingListReservation](
|
||||||
|
[ID] [tinyint] NOT NULL,
|
||||||
|
[Documento] [varchar](8) NULL,
|
||||||
|
[IDOperatore] [int] NULL,
|
||||||
|
[ModUtente] [varchar](50) NULL,
|
||||||
|
[ModDataOra] [datetime] NULL,
|
||||||
|
CONSTRAINT [PK_PickingListReservation] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||||
|
CONSTRAINT [CK_PickingListReservation_Singleton] CHECK ([ID] = 1)
|
||||||
|
) ON [PRIMARY];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END
|
||||||
|
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 @DocumentoAttivo varchar(8) = NULL;
|
||||||
|
DECLARE @Description varchar(255) = '';
|
||||||
|
DECLARE @Message varchar(255) = '';
|
||||||
|
DECLARE @IDResult int = 0;
|
||||||
|
DECLARE @ID int = 0;
|
||||||
|
|
||||||
|
SELECT @Nominativo = [Login]
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
IF @Azione NOT IN ('P', 'S')
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -10;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SET @RC = -20;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.PickingListReservation WHERE ID = 1)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.PickingListReservation (ID, Documento, IDOperatore, ModUtente, ModDataOra)
|
||||||
|
VALUES (1, NULL, NULL, NULL, GETDATE());
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT @DocumentoAttivo = NULLIF(LTRIM(RTRIM(Documento)), '')
|
||||||
|
FROM dbo.PickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
DECLARE @TargetCelle TABLE (
|
||||||
|
IDCella int PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @TargetCelle (IDCella)
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL;
|
||||||
|
|
||||||
|
IF @Azione = 'P'
|
||||||
|
BEGIN
|
||||||
|
IF @DocumentoAttivo = @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 0,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE ISNULL(c.IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = @Documento,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
SELECT TOP 1 @Description = NAZIONE
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento;
|
||||||
|
|
||||||
|
EXEC dbo.sp_LogPackingList
|
||||||
|
@ID = @ID,
|
||||||
|
@Code = @Documento,
|
||||||
|
@Description = @Description,
|
||||||
|
@Message = @Message OUTPUT,
|
||||||
|
@IDResult = @IDResult OUTPUT;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @Azione = 'S'
|
||||||
|
BEGIN
|
||||||
|
IF ISNULL(@DocumentoAttivo, '') <> @Documento
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER VIEW [dbo].[XMag_ViewPackingList]
|
||||||
|
AS
|
||||||
|
WITH Base AS (
|
||||||
|
SELECT
|
||||||
|
dbo.vPreparaPackingList.UDC AS Pallet,
|
||||||
|
dbo.vPreparaPackingList.NUMLOT AS Lotto,
|
||||||
|
dbo.vPreparaPackingList.CODICE AS Articolo,
|
||||||
|
dbo.vPreparaPackingList.DESCR AS Descrizione,
|
||||||
|
dbo.vPreparaPackingList.Qta,
|
||||||
|
dbo.vPreparaPackingList.NUMDOC AS Documento,
|
||||||
|
dbo.vPreparaPackingList.Expr1 AS CodNazione,
|
||||||
|
dbo.vPreparaPackingList.NAZIONE,
|
||||||
|
CASE
|
||||||
|
WHEN Expr1 = 'DE' THEN 10
|
||||||
|
WHEN Expr1 = 'TH' THEN CASE WHEN SUBSTRING(dbo.vPreparaPackingList.DESCRDEST, 1, 2) = 'NA' THEN 11 ELSE 13 END
|
||||||
|
WHEN Expr1 = 'MEX' THEN 12
|
||||||
|
ELSE 4
|
||||||
|
END AS Stato,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.NumeroPallet, 0) AS PalletCella,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDMagazzino, 1) AS Magazzino,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDArea, 5) AS Area,
|
||||||
|
ISNULL(dbo.XMag_GiacenzaPallet.IDCella, 1000) AS Cella,
|
||||||
|
ISNULL(dbo.Celle.Ordinamento, 99999) AS Ordinamento,
|
||||||
|
ISNULL(dbo.Celle.Corsia + ' - ' + dbo.Celle.Colonna + ' - ' + dbo.Celle.Fila, 'Non scaff.') AS Ubicazione,
|
||||||
|
SUBSTRING(dbo.vPreparaPackingList.DESCRDEST, 1, 2) AS DEST
|
||||||
|
FROM dbo.Celle
|
||||||
|
INNER JOIN dbo.XMag_GiacenzaPallet
|
||||||
|
ON dbo.Celle.ID = dbo.XMag_GiacenzaPallet.IDCella
|
||||||
|
RIGHT OUTER JOIN dbo.vPreparaPackingList
|
||||||
|
ON dbo.XMag_GiacenzaPallet.BarcodePallet COLLATE SQL_Latin1_General_CP1_CI_AS = dbo.vPreparaPackingList.UDC
|
||||||
|
GROUP BY
|
||||||
|
dbo.vPreparaPackingList.Expr1,
|
||||||
|
dbo.vPreparaPackingList.NAZIONE,
|
||||||
|
dbo.vPreparaPackingList.UDC,
|
||||||
|
dbo.vPreparaPackingList.NUMDOC,
|
||||||
|
dbo.vPreparaPackingList.NUMLOT,
|
||||||
|
dbo.vPreparaPackingList.CODICE,
|
||||||
|
dbo.vPreparaPackingList.DESCR,
|
||||||
|
dbo.vPreparaPackingList.Qta,
|
||||||
|
dbo.XMag_GiacenzaPallet.NumeroPallet,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDMagazzino,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDArea,
|
||||||
|
dbo.XMag_GiacenzaPallet.IDCella,
|
||||||
|
dbo.Celle.Ordinamento,
|
||||||
|
dbo.Celle.Corsia,
|
||||||
|
dbo.Celle.Colonna,
|
||||||
|
dbo.Celle.Fila,
|
||||||
|
dbo.vPreparaPackingList.DESCRDEST
|
||||||
|
)
|
||||||
|
SELECT TOP 10000
|
||||||
|
Base.Pallet,
|
||||||
|
Base.Lotto,
|
||||||
|
Base.Articolo,
|
||||||
|
Base.Descrizione,
|
||||||
|
Base.Qta,
|
||||||
|
Base.Documento,
|
||||||
|
Base.CodNazione,
|
||||||
|
Base.NAZIONE,
|
||||||
|
Base.Stato,
|
||||||
|
Base.PalletCella,
|
||||||
|
Base.Magazzino,
|
||||||
|
Base.Area,
|
||||||
|
Base.Cella,
|
||||||
|
Base.Ordinamento,
|
||||||
|
Base.Ubicazione,
|
||||||
|
Base.DEST,
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(Base.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS IDStato
|
||||||
|
FROM Base
|
||||||
|
LEFT JOIN dbo.PickingListReservation pr
|
||||||
|
ON pr.ID = 1
|
||||||
|
AND NULLIF(LTRIM(RTRIM(pr.Documento)), '') IS NOT NULL
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN pr.Documento IS NOT NULL
|
||||||
|
AND pr.Documento = CAST(Base.Documento AS varchar(8))
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END DESC,
|
||||||
|
Base.Documento,
|
||||||
|
Base.Ordinamento;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[sp_xExePackingListPalletPrenota]
|
||||||
|
@IDOperatore int,
|
||||||
|
@Documento varchar(8),
|
||||||
|
@RC int OUTPUT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET @RC = 0;
|
||||||
|
|
||||||
|
DECLARE @Nominativo varchar(50) = '';
|
||||||
|
|
||||||
|
SELECT @Nominativo = LOGIN
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE ID = @IDOperatore;
|
||||||
|
|
||||||
|
IF @Nominativo IS NULL
|
||||||
|
SET @Nominativo = 'SYSTEM';
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET c.IDStato = 1,
|
||||||
|
c.ModUtente = @Nominativo,
|
||||||
|
c.ModDataOra = GETDATE()
|
||||||
|
FROM dbo.Celle c
|
||||||
|
WHERE c.ID IN (
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
AND Cella IS NOT NULL
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[sp_ControllaPrenotazionePackingListPalletNew]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Documento varchar(8) = NULL;
|
||||||
|
DECLARE @IDOperatore int = 0;
|
||||||
|
DECLARE @Nominativo varchar(50) = 'SYSTEM';
|
||||||
|
DECLARE @RC int = 0;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
@Documento = NULLIF(LTRIM(RTRIM(Documento)), ''),
|
||||||
|
@IDOperatore = ISNULL(IDOperatore, 0),
|
||||||
|
@Nominativo = ISNULL(NULLIF(LTRIM(RTRIM(ModUtente)), ''), 'SYSTEM')
|
||||||
|
FROM dbo.PickingListReservation
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
IF ISNULL(@Documento, '') = ''
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ViewPackingListRestante
|
||||||
|
WHERE CAST(Documento AS varchar(8)) = @Documento
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
UPDATE dbo.PickingListReservation
|
||||||
|
SET Documento = NULL,
|
||||||
|
IDOperatore = @IDOperatore,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = 1;
|
||||||
|
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = @Nominativo,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ISNULL(IDStato, 0) <> 0;
|
||||||
|
|
||||||
|
IF @IDOperatore <= 0
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP 1 @IDOperatore = ID
|
||||||
|
FROM dbo.Operatori
|
||||||
|
WHERE LOGIN = @Nominativo;
|
||||||
|
END;
|
||||||
|
|
||||||
|
EXEC dbo.sp_xExePackingListPalletPrenota
|
||||||
|
@IDOperatore = @IDOperatore,
|
||||||
|
@Documento = @Documento,
|
||||||
|
@RC = @RC OUTPUT;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@@ -2,9 +2,150 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except Exception: # pragma: no cover - safety fallback if dependency is missing locally
|
||||||
|
class _FallbackLogger:
|
||||||
|
"""Minimal adapter used only when Loguru is not installed yet."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__)
|
||||||
|
self._logger.setLevel(logging.DEBUG)
|
||||||
|
self._logger.propagate = False
|
||||||
|
|
||||||
|
def bind(self, **_kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs):
|
||||||
|
handler: logging.Handler
|
||||||
|
if hasattr(sink, "write"):
|
||||||
|
handler = logging.StreamHandler(sink)
|
||||||
|
else:
|
||||||
|
handler = logging.FileHandler(str(sink), encoding=encoding)
|
||||||
|
handler.setLevel(getattr(logging, str(level).upper(), logging.INFO))
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
||||||
|
)
|
||||||
|
self._logger.addHandler(handler)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def log(self, level, message):
|
||||||
|
getattr(self._logger, str(level).lower(), self._logger.info)(message)
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def exception(self, message):
|
||||||
|
self._logger.exception(message)
|
||||||
|
|
||||||
|
logger = _FallbackLogger()
|
||||||
|
|
||||||
|
|
||||||
|
PACKINGLIST_SP_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG"
|
||||||
|
MODULE_LOG_NAME = Path(__file__).stem
|
||||||
|
MODULE_LOG_PATH = Path(__file__).with_suffix(".log")
|
||||||
|
_MODULE_LOG_ENABLED = PACKINGLIST_SP_LOG_MODE.upper() != "OFF"
|
||||||
|
_MODULE_LOG_LEVEL = "DEBUG" if PACKINGLIST_SP_LOG_MODE.upper() == "DEBUG" else "INFO"
|
||||||
|
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_module_logger():
|
||||||
|
"""Configure console and file logging for this module."""
|
||||||
|
global _MODULE_LOGGING_CONFIGURED
|
||||||
|
if _MODULE_LOGGING_CONFIGURED:
|
||||||
|
return
|
||||||
|
if not _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
sys.stderr,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=True,
|
||||||
|
filter=record_filter,
|
||||||
|
format=(
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>" + MODULE_LOG_NAME + "</cyan> | "
|
||||||
|
"<level>{message}</level>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
MODULE_LOG_PATH,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
filter=record_filter,
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}",
|
||||||
|
)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
|
||||||
|
|
||||||
|
def _format_payload(payload: Any) -> str:
|
||||||
|
"""Serialize payloads for human-readable logging."""
|
||||||
|
try:
|
||||||
|
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||||
|
except Exception:
|
||||||
|
return repr(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_call(level: Optional[str] = None):
|
||||||
|
"""Trace entry, exit and failure of selected procedure helpers."""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
effective_level = level or _MODULE_LOG_LEVEL
|
||||||
|
_MODULE_LOGGER.log(
|
||||||
|
effective_level,
|
||||||
|
f"CALL {func.__qualname__} args={_format_payload(args[1:] if len(args) > 1 else ())} kwargs={_format_payload(kwargs)}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
_MODULE_LOGGER.exception(f"FAIL {func.__qualname__}")
|
||||||
|
raise
|
||||||
|
_MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}")
|
||||||
|
return result
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _log_sql(query_name: str, sql: str, params: Dict[str, Any]):
|
||||||
|
"""Log one SQL statement and its parameters."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params)}")
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _log_dataset(query_name: str, rows: Any):
|
||||||
|
"""Log query results at summary or full-debug level depending on the mode."""
|
||||||
|
if isinstance(rows, list):
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows")
|
||||||
|
if PACKINGLIST_SP_LOG_MODE.upper() == "DEBUG":
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}")
|
||||||
|
else:
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} scalar={_format_payload(rows)}")
|
||||||
|
|
||||||
|
|
||||||
|
_configure_module_logger()
|
||||||
|
if _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={PACKINGLIST_SP_LOG_MODE.upper()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SPResult:
|
class SPResult:
|
||||||
@@ -15,14 +156,18 @@ class SPResult:
|
|||||||
id_result: Optional[int] = None
|
id_result: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any]:
|
async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any]:
|
||||||
"""Return the first column of the first row from a query result."""
|
"""Return the first column of the first row from a query result."""
|
||||||
|
_log_sql("_query_one_value", sql, params)
|
||||||
if hasattr(db, "query_json"):
|
if hasattr(db, "query_json"):
|
||||||
res = await db.query_json(sql, params)
|
res = await db.query_json(sql, params)
|
||||||
if isinstance(res, list) and res:
|
if isinstance(res, list) and res:
|
||||||
row0 = res[0]
|
row0 = res[0]
|
||||||
if isinstance(row0, dict):
|
if isinstance(row0, dict):
|
||||||
return next(iter(row0.values()), None)
|
value = next(iter(row0.values()), None)
|
||||||
|
_log_dataset("_query_one_value", value)
|
||||||
|
return value
|
||||||
elif isinstance(res, dict):
|
elif isinstance(res, dict):
|
||||||
rows = None
|
rows = None
|
||||||
for key in ("rows", "data", "result", "records"):
|
for key in ("rows", "data", "result", "records"):
|
||||||
@@ -32,139 +177,126 @@ async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any
|
|||||||
if rows:
|
if rows:
|
||||||
row0 = rows[0]
|
row0 = rows[0]
|
||||||
if isinstance(row0, dict):
|
if isinstance(row0, dict):
|
||||||
return next(iter(row0.values()), None)
|
value = next(iter(row0.values()), None)
|
||||||
|
_log_dataset("_query_one_value", value)
|
||||||
|
return value
|
||||||
if isinstance(row0, (list, tuple)) and row0:
|
if isinstance(row0, (list, tuple)) and row0:
|
||||||
return row0[0]
|
value = row0[0]
|
||||||
|
_log_dataset("_query_one_value", value)
|
||||||
|
return value
|
||||||
|
_log_dataset("_query_one_value", None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if hasattr(db, "query_value"):
|
if hasattr(db, "query_value"):
|
||||||
return await db.query_value(sql, params)
|
value = await db.query_value(sql, params)
|
||||||
|
_log_dataset("_query_one_value", value)
|
||||||
|
return value
|
||||||
if hasattr(db, "scalar"):
|
if hasattr(db, "scalar"):
|
||||||
return await db.scalar(sql, params)
|
value = await db.scalar(sql, params)
|
||||||
|
_log_dataset("_query_one_value", value)
|
||||||
|
return value
|
||||||
raise RuntimeError("Il client DB non espone query_json/query_value/scalar")
|
raise RuntimeError("Il client DB non espone query_json/query_value/scalar")
|
||||||
|
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Return all rows as dictionaries, normalizing different DB client APIs."""
|
"""Return all rows as dictionaries, normalizing different DB client APIs."""
|
||||||
|
_log_sql("_query_all", sql, params)
|
||||||
if hasattr(db, "query_json"):
|
if hasattr(db, "query_json"):
|
||||||
res = await db.query_json(sql, params)
|
res = await db.query_json(sql, params)
|
||||||
if res is None:
|
if res is None:
|
||||||
|
_log_dataset("_query_all", [])
|
||||||
return []
|
return []
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
return res if res and isinstance(res[0], dict) else []
|
rows = res if res and isinstance(res[0], dict) else []
|
||||||
|
_log_dataset("_query_all", rows)
|
||||||
|
return rows
|
||||||
if isinstance(res, dict):
|
if isinstance(res, dict):
|
||||||
for key in ("rows", "data", "result", "records"):
|
for key in ("rows", "data", "result", "records"):
|
||||||
if key in res and isinstance(res[key], list):
|
if key in res and isinstance(res[key], list):
|
||||||
rows = res[key]
|
rows = res[key]
|
||||||
if rows and isinstance(rows[0], dict):
|
if rows and isinstance(rows[0], dict):
|
||||||
|
_log_dataset("_query_all", rows)
|
||||||
return rows
|
return rows
|
||||||
cols = res.get("columns") or res.get("cols") or []
|
cols = res.get("columns") or res.get("cols") or []
|
||||||
out = []
|
out = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if isinstance(row, (list, tuple)) and cols:
|
if isinstance(row, (list, tuple)) and cols:
|
||||||
out.append({(cols[i] if i < len(cols) else f"c{i}"): row[i] for i in range(min(len(cols), len(row)))})
|
out.append({(cols[i] if i < len(cols) else f"c{i}"): row[i] for i in range(min(len(cols), len(row)))})
|
||||||
|
_log_dataset("_query_all", out)
|
||||||
return out
|
return out
|
||||||
|
_log_dataset("_query_all", [])
|
||||||
return []
|
return []
|
||||||
if hasattr(db, "fetch_all"):
|
if hasattr(db, "fetch_all"):
|
||||||
return await db.fetch_all(sql, params)
|
rows = await db.fetch_all(sql, params)
|
||||||
|
_log_dataset("_query_all", rows)
|
||||||
|
return rows
|
||||||
raise RuntimeError("Il client DB non espone query_json/fetch_all")
|
raise RuntimeError("Il client DB non espone query_json/fetch_all")
|
||||||
|
|
||||||
|
|
||||||
|
@_log_call("DEBUG")
|
||||||
async def _execute(db, sql: str, params: Dict[str, Any]) -> int:
|
async def _execute(db, sql: str, params: Dict[str, Any]) -> int:
|
||||||
"""Execute a DML statement using the best method exposed by the DB client."""
|
"""Execute a DML statement using the best method exposed by the DB client."""
|
||||||
|
_log_sql("_execute", sql, params)
|
||||||
for name in ("execute", "exec", "execute_non_query"):
|
for name in ("execute", "exec", "execute_non_query"):
|
||||||
if hasattr(db, name):
|
if hasattr(db, name):
|
||||||
rc = await getattr(db, name)(sql, params)
|
rc = await getattr(db, name)(sql, params)
|
||||||
if isinstance(rc, int):
|
if isinstance(rc, int):
|
||||||
|
_log_dataset("_execute", rc)
|
||||||
return rc
|
return rc
|
||||||
|
_log_dataset("_execute", 0)
|
||||||
return 0
|
return 0
|
||||||
if hasattr(db, "query_json"):
|
if hasattr(db, "query_json"):
|
||||||
await db.query_json(sql, params)
|
await db.query_json(sql, params)
|
||||||
|
_log_dataset("_execute", 0)
|
||||||
return 0
|
return 0
|
||||||
raise RuntimeError("Il client DB non espone metodi di esecuzione DML noti")
|
raise RuntimeError("Il client DB non espone metodi di esecuzione DML noti")
|
||||||
|
|
||||||
|
|
||||||
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) -> SPResult:
|
@_log_call()
|
||||||
"""Toggle the reservation state of all cells belonging to a packing list.
|
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str, Azione: str = "P") -> SPResult:
|
||||||
|
"""Execute the Python-specific picking-list reservation stored procedure."""
|
||||||
The implementation mirrors the original SQL stored procedure while using
|
|
||||||
the shared async DB client already managed by the application.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
nominativo = await _query_one_value(
|
azione = str(Azione or "P").strip().upper()
|
||||||
db,
|
if azione not in ("P", "S"):
|
||||||
"SELECT LOGIN FROM Operatori WHERE id = :IDOperatore",
|
return SPResult(rc=-10, message=f"Azione non valida: {Azione}", id_result=None)
|
||||||
{"IDOperatore": IDOperatore},
|
_MODULE_LOGGER.log(
|
||||||
) or ""
|
_MODULE_LOG_LEVEL,
|
||||||
|
f"Procedura packing list via stored procedure documento={Documento} azione={azione} id_operatore={IDOperatore}",
|
||||||
|
)
|
||||||
|
sql = """
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
DECLARE @RC int = 0;
|
||||||
|
|
||||||
celle = await _query_all(
|
EXEC dbo.py_sp_xExePackingListPallet
|
||||||
db,
|
@IDOperatore = :IDOperatore,
|
||||||
"""
|
@Documento = :Documento,
|
||||||
SELECT DISTINCT Cella
|
@Azione = :Azione,
|
||||||
FROM dbo.XMag_ViewPackingList
|
@RC = @RC OUTPUT;
|
||||||
WHERE Documento = :Documento
|
|
||||||
""",
|
|
||||||
{"Documento": Documento},
|
|
||||||
)
|
|
||||||
id_celle = [row.get("Cella") for row in celle if "Cella" in row]
|
|
||||||
|
|
||||||
# Each cell is toggled individually because the original procedure also
|
SELECT CAST(@RC AS int) AS RC;
|
||||||
# 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},
|
|
||||||
)
|
|
||||||
if stato == 0:
|
|
||||||
await _execute(
|
|
||||||
db,
|
|
||||||
"""
|
"""
|
||||||
UPDATE Celle
|
_log_sql("py_sp_xExePackingListPallet", sql, {"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione})
|
||||||
SET IDStato = 1,
|
if not hasattr(db, "query_json"):
|
||||||
ModUtente = :N,
|
raise RuntimeError("Il client DB non espone query_json necessario per eseguire la stored procedure.")
|
||||||
ModDataOra = GETDATE()
|
res = await db.query_json(
|
||||||
WHERE ID = :IDC
|
sql,
|
||||||
""",
|
{"IDOperatore": IDOperatore, "Documento": Documento, "Azione": azione},
|
||||||
{"N": nominativo, "IDC": id_cella},
|
as_dict_rows=True,
|
||||||
|
commit=True,
|
||||||
)
|
)
|
||||||
else:
|
rows = []
|
||||||
await _execute(
|
if isinstance(res, dict):
|
||||||
db,
|
rows = res.get("rows", []) or []
|
||||||
"""
|
_log_dataset("py_sp_xExePackingListPallet", rows)
|
||||||
UPDATE Celle
|
rc = 0
|
||||||
SET IDStato = 0,
|
if rows and isinstance(rows[0], dict):
|
||||||
ModUtente = :N,
|
try:
|
||||||
ModDataOra = GETDATE()
|
rc = int(rows[0].get("RC") or 0)
|
||||||
WHERE ID = :IDC
|
except Exception:
|
||||||
""",
|
rc = 0
|
||||||
{"N": nominativo, "IDC": id_cella},
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Stored procedure completata documento={Documento} azione={azione} rc={rc}")
|
||||||
)
|
return SPResult(rc=rc, message="", id_result=None)
|
||||||
|
|
||||||
description = await _query_one_value(
|
|
||||||
db,
|
|
||||||
"""
|
|
||||||
SELECT TOP 1 NAZIONE
|
|
||||||
FROM dbo.XMag_ViewPackingList
|
|
||||||
WHERE Documento = :Documento
|
|
||||||
GROUP BY Documento, NAZIONE
|
|
||||||
ORDER BY NAZIONE
|
|
||||||
""",
|
|
||||||
{"Documento": Documento},
|
|
||||||
)
|
|
||||||
|
|
||||||
await _execute(
|
|
||||||
db,
|
|
||||||
"""
|
|
||||||
INSERT INTO dbo.LogPackingList (Code, Description, IDInsUser, InsDateTime)
|
|
||||||
VALUES (:Code, :Descr, :IDInsUser, GETDATE());
|
|
||||||
""",
|
|
||||||
{"Code": Documento, "Descr": description, "IDInsUser": IDOperatore},
|
|
||||||
)
|
|
||||||
|
|
||||||
new_id = await _query_one_value(db, "SELECT SCOPE_IDENTITY() AS ID", {})
|
|
||||||
return SPResult(rc=0, message="", id_result=int(new_id) if new_id is not None else None)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
_MODULE_LOGGER.exception(f"Procedura fallita documento={Documento} azione={Azione}: {exc}")
|
||||||
return SPResult(rc=-1, message=str(exc), id_result=None)
|
return SPResult(rc=-1, message=str(exc), id_result=None)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ requires-python = ">=3.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlalchemy[asyncio]>=2.0",
|
"sqlalchemy[asyncio]>=2.0",
|
||||||
"aioodbc>=0.3.3",
|
"aioodbc>=0.3.3",
|
||||||
|
"loguru>=0.7",
|
||||||
|
"tksheet>=7.5",
|
||||||
# "orjson>=3.9" # opzionale: il tuo codice fa fallback su json puro
|
# "orjson>=3.9" # opzionale: il tuo codice fa fallback su json puro
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
aioodbc==0.5.0
|
||||||
|
customtkinter==5.2.2
|
||||||
|
loguru
|
||||||
|
openpyxl==3.1.5
|
||||||
|
orjson
|
||||||
|
pyodbc==5.3.0
|
||||||
|
SQLAlchemy==2.0.49
|
||||||
|
tksheet==7.6.0
|
||||||
539
reset_corsie.py
539
reset_corsie.py
@@ -1,16 +1,167 @@
|
|||||||
"""Window used to inspect and empty an entire warehouse aisle.
|
"""Window used to inspect and logically empty an entire warehouse aisle.
|
||||||
|
|
||||||
The module exposes a destructive maintenance tool: it summarizes the occupancy
|
The tool summarizes the current occupancy of one aisle and, after explicit
|
||||||
state of a selected aisle and, after explicit confirmation, deletes matching
|
confirmation, unloads every active UDC through the same logical movement
|
||||||
rows from ``MagazziniPallet``.
|
semantics used by the rest of the WMS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
from tkinter import messagebox, simpledialog, ttk
|
from tkinter import messagebox, simpledialog, ttk
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
from gestione_aree_frame_async import AsyncRunner, BusyOverlay
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except Exception: # pragma: no cover - fallback used only when Loguru is not available
|
||||||
|
class _FallbackLogger:
|
||||||
|
"""Minimal adapter used only when Loguru is not installed yet."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__)
|
||||||
|
self._logger.setLevel(logging.DEBUG)
|
||||||
|
self._logger.propagate = False
|
||||||
|
|
||||||
|
def bind(self, **_kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs):
|
||||||
|
handler: logging.Handler
|
||||||
|
if hasattr(sink, "write"):
|
||||||
|
handler = logging.StreamHandler(sink)
|
||||||
|
else:
|
||||||
|
handler = logging.FileHandler(str(sink), encoding=encoding)
|
||||||
|
handler.setLevel(getattr(logging, str(level).upper(), logging.INFO))
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
||||||
|
)
|
||||||
|
self._logger.addHandler(handler)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def log(self, level, message):
|
||||||
|
getattr(self._logger, str(level).lower(), self._logger.info)(message)
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def exception(self, message):
|
||||||
|
self._logger.exception(message)
|
||||||
|
|
||||||
|
logger = _FallbackLogger()
|
||||||
|
|
||||||
|
|
||||||
|
RESET_CORSIE_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG"
|
||||||
|
MODULE_LOG_NAME = Path(__file__).stem
|
||||||
|
MODULE_LOG_PATH = Path(__file__).with_suffix(".log")
|
||||||
|
_MODULE_LOG_ENABLED = RESET_CORSIE_LOG_MODE.upper() != "OFF"
|
||||||
|
_MODULE_LOG_LEVEL = "DEBUG" if RESET_CORSIE_LOG_MODE.upper() == "DEBUG" else "INFO"
|
||||||
|
_MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_module_logger():
|
||||||
|
"""Configure console and file logging for this module."""
|
||||||
|
global _MODULE_LOGGING_CONFIGURED
|
||||||
|
if _MODULE_LOGGING_CONFIGURED:
|
||||||
|
return
|
||||||
|
if not _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
sys.stderr,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=True,
|
||||||
|
filter=record_filter,
|
||||||
|
format=(
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>" + MODULE_LOG_NAME + "</cyan> | "
|
||||||
|
"<level>{message}</level>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
MODULE_LOG_PATH,
|
||||||
|
level=_MODULE_LOG_LEVEL,
|
||||||
|
colorize=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
filter=record_filter,
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}",
|
||||||
|
)
|
||||||
|
_MODULE_LOGGING_CONFIGURED = True
|
||||||
|
|
||||||
|
|
||||||
|
def _format_payload(payload: Any) -> str:
|
||||||
|
"""Serialize payloads for human-readable logging."""
|
||||||
|
try:
|
||||||
|
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||||
|
except Exception:
|
||||||
|
return repr(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_call(level: str | None = None):
|
||||||
|
"""Trace entry, exit and failure of selected high-level functions."""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
effective_level = level or _MODULE_LOG_LEVEL
|
||||||
|
_MODULE_LOGGER.log(
|
||||||
|
effective_level,
|
||||||
|
f"CALL {func.__qualname__} args={_format_payload(args[1:] if args else ())} kwargs={_format_payload(kwargs)}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
_MODULE_LOGGER.exception(f"FAIL {func.__qualname__}")
|
||||||
|
raise
|
||||||
|
_MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _log_sql(query_name: str, sql: str, params: dict[str, Any] | None = None):
|
||||||
|
"""Log one SQL statement and its parameters."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params or {})}")
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _log_dataset(query_name: str, rows: list[Any]):
|
||||||
|
"""Log query results at summary or full-debug level depending on the flag."""
|
||||||
|
_MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows")
|
||||||
|
if RESET_CORSIE_LOG_MODE.upper() == "DEBUG":
|
||||||
|
_MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}")
|
||||||
|
|
||||||
|
|
||||||
|
_configure_module_logger()
|
||||||
|
if _MODULE_LOG_ENABLED:
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={RESET_CORSIE_LOG_MODE.upper()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SQL_CORSIE = """
|
SQL_CORSIE = """
|
||||||
WITH C AS (
|
WITH C AS (
|
||||||
@@ -76,59 +227,192 @@ WHERE COALESCE(s.n,0) > 0
|
|||||||
ORDER BY TRY_CONVERT(int,c.Colonna), c.Colonna, TRY_CONVERT(int,c.Fila), c.Fila;
|
ORDER BY TRY_CONVERT(int,c.Colonna), c.Colonna, TRY_CONVERT(int,c.Fila), c.Fila;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SQL_COUNT_DELETE = """
|
SQL_COUNT_RESET = """
|
||||||
SELECT COUNT(*) AS RowsToDelete
|
SELECT
|
||||||
FROM dbo.MagazziniPallet mp
|
COUNT(DISTINCT g.BarcodePallet) AS TotUDC,
|
||||||
JOIN dbo.Celle c ON c.ID = mp.IDCella
|
COUNT(DISTINCT g.IDCella) AS TotCelle
|
||||||
WHERE c.ID <> 9999 AND LTRIM(RTRIM(c.Corsia)) = :corsia;
|
FROM dbo.XMag_GiacenzaPallet g
|
||||||
|
JOIN dbo.Celle c ON c.ID = g.IDCella
|
||||||
|
WHERE c.ID <> 9999
|
||||||
|
AND LTRIM(RTRIM(c.Corsia)) = :corsia;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SQL_DELETE = """
|
SQL_UDC_RESET = """
|
||||||
DELETE mp
|
WITH U AS (
|
||||||
FROM dbo.MagazziniPallet mp
|
SELECT DISTINCT
|
||||||
JOIN dbo.Celle c ON c.ID = mp.IDCella
|
g.BarcodePallet AS BarcodePallet,
|
||||||
WHERE c.ID <> 9999 AND LTRIM(RTRIM(c.Corsia)) = :corsia;
|
g.IDCella AS IDCella,
|
||||||
|
CONCAT(LTRIM(RTRIM(c.Corsia)), '.', LTRIM(RTRIM(c.Colonna)), '.', LTRIM(RTRIM(c.Fila))) AS Ubicazione,
|
||||||
|
TRY_CONVERT(int, c.Colonna) AS SortColNum,
|
||||||
|
LTRIM(RTRIM(c.Colonna)) AS SortColTxt,
|
||||||
|
TRY_CONVERT(int, c.Fila) AS SortFilaNum,
|
||||||
|
LTRIM(RTRIM(c.Fila)) AS SortFilaTxt
|
||||||
|
FROM dbo.XMag_GiacenzaPallet g
|
||||||
|
JOIN dbo.Celle c ON c.ID = g.IDCella
|
||||||
|
WHERE c.ID <> 9999
|
||||||
|
AND LTRIM(RTRIM(c.Corsia)) = :corsia
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
BarcodePallet,
|
||||||
|
IDCella,
|
||||||
|
Ubicazione
|
||||||
|
FROM U
|
||||||
|
ORDER BY
|
||||||
|
SortColNum,
|
||||||
|
SortColTxt,
|
||||||
|
SortFilaNum,
|
||||||
|
SortFilaTxt,
|
||||||
|
BarcodePallet;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ResetCorsieWindow(ctk.CTkToplevel):
|
class ResetCorsieWindow(ctk.CTkToplevel):
|
||||||
"""Toplevel used to inspect and clear the pallets assigned to an aisle."""
|
"""Toplevel used to inspect and clear the pallets assigned to an aisle."""
|
||||||
|
|
||||||
def __init__(self, parent, db_client):
|
@_log_call()
|
||||||
|
def __init__(self, parent, db_client, session=None):
|
||||||
"""Create the window and immediately load the list of aisles."""
|
"""Create the window and immediately load the list of aisles."""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title("Reset Corsie - svuotamento celle per corsia")
|
self._theme = theme_section("reset_corsie", {})
|
||||||
self.geometry("1000x680")
|
self._locale_catalog = load_locale_catalog()
|
||||||
self.minsize(880, 560)
|
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]))
|
||||||
self.resizable(True, True)
|
self.resizable(True, True)
|
||||||
|
try:
|
||||||
|
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
self.db = db_client
|
self.db = db_client
|
||||||
self._busy = BusyOverlay(self)
|
self.session = session
|
||||||
|
self._busy = InlineBusyOverlay(self, self._theme)
|
||||||
self._async = AsyncRunner(self)
|
self._async = AsyncRunner(self)
|
||||||
|
self._refresh_token = 0
|
||||||
|
self._tooltip_catalog = load_tooltip_catalog()
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._load_corsie()
|
self._load_corsie()
|
||||||
|
|
||||||
|
def _setup_tree_style(self):
|
||||||
|
"""Apply a denser, spreadsheet-like style to the main result grid."""
|
||||||
|
|
||||||
|
style = ttk.Style(self)
|
||||||
|
style.configure(
|
||||||
|
"ResetCorsie.Treeview.Heading",
|
||||||
|
font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")),
|
||||||
|
background=theme_value(self._theme, "tree_heading_bg", "#b8c7db"),
|
||||||
|
foreground=theme_value(self._theme, "tree_heading_fg", "#10243e"),
|
||||||
|
relief="flat",
|
||||||
|
padding=theme_padding(self._theme, "tree_heading_padding", (8, 6)),
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"ResetCorsie.Treeview.Heading",
|
||||||
|
background=[("active", theme_value(self._theme, "tree_heading_bg_active", "#aebfd6"))],
|
||||||
|
relief=[("pressed", "groove"), ("!pressed", "flat")],
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"ResetCorsie.Treeview",
|
||||||
|
font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)),
|
||||||
|
rowheight=int(theme_value(self._theme, "tree_row_height", 30)),
|
||||||
|
background=theme_value(self._theme, "tree_body_bg", "#ffffff"),
|
||||||
|
fieldbackground=theme_value(self._theme, "tree_body_bg", "#ffffff"),
|
||||||
|
foreground=theme_value(self._theme, "tree_body_fg", "#1f1f1f"),
|
||||||
|
borderwidth=0,
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"ResetCorsie.Treeview",
|
||||||
|
background=[("selected", theme_value(self._theme, "tree_selected_bg", "#cfe4ff"))],
|
||||||
|
foreground=[("selected", theme_value(self._theme, "tree_selected_fg", "#10243e"))],
|
||||||
|
)
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
"""Create selectors, summary widgets and the occupied-cell grid."""
|
"""Create selectors, summary widgets and the occupied-cell grid."""
|
||||||
|
self._setup_tree_style()
|
||||||
top = ctk.CTkFrame(self)
|
top = ctk.CTkFrame(self)
|
||||||
top.pack(fill="x", padx=8, pady=8)
|
top.pack(
|
||||||
ctk.CTkLabel(top, text="Corsia:").pack(side="left")
|
fill="x",
|
||||||
self.cmb = ctk.CTkComboBox(top, width=140, values=[])
|
padx=int(theme_value(self._theme, "frame_padx", 8)),
|
||||||
|
pady=int(theme_value(self._theme, "frame_pady", 8)),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
top.configure(fg_color=theme_color(self._theme, "top_frame_fg_color", ("#d7d7d7", "#3b3b3b")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ctk.CTkLabel(
|
||||||
|
top,
|
||||||
|
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(
|
||||||
|
top,
|
||||||
|
width=int(theme_value(self._theme, "combobox_width", 140)),
|
||||||
|
height=int(theme_value(self._theme, "combobox_height", 28)),
|
||||||
|
values=[],
|
||||||
|
font=theme_font(self._theme, "combobox_font", ("Segoe UI", 10)),
|
||||||
|
dropdown_font=theme_font(self._theme, "combobox_font", ("Segoe UI", 10)),
|
||||||
|
)
|
||||||
self.cmb.pack(side="left", padx=(6, 10))
|
self.cmb.pack(side="left", padx=(6, 10))
|
||||||
ctk.CTkButton(top, text="Carica", command=self.refresh).pack(side="left")
|
btn_refresh = ctk.CTkButton(
|
||||||
ctk.CTkButton(top, text="Svuota corsia...", command=self._ask_reset).pack(side="right")
|
top,
|
||||||
|
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)),
|
||||||
|
corner_radius=int(theme_value(self._theme, "toolbar_button_corner_radius", 6)),
|
||||||
|
font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")),
|
||||||
|
)
|
||||||
|
btn_refresh.pack(side="left")
|
||||||
|
btn_reset = ctk.CTkButton(
|
||||||
|
top,
|
||||||
|
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)),
|
||||||
|
corner_radius=int(theme_value(self._theme, "toolbar_button_corner_radius", 6)),
|
||||||
|
font=theme_font(self._theme, "toolbar_button_font", ("Segoe UI", 10, "bold")),
|
||||||
|
)
|
||||||
|
btn_reset.pack(side="right")
|
||||||
|
WidgetToolTip(btn_refresh, tooltip_text("reset_corsie.refresh", catalog=self._tooltip_catalog))
|
||||||
|
WidgetToolTip(btn_reset, tooltip_text("reset_corsie.empty_aisle", catalog=self._tooltip_catalog))
|
||||||
|
|
||||||
mid = ctk.CTkFrame(self)
|
mid = ctk.CTkFrame(self)
|
||||||
mid.pack(fill="both", expand=True, padx=8, pady=(0, 8))
|
mid.pack(
|
||||||
|
fill="both",
|
||||||
|
expand=True,
|
||||||
|
padx=int(theme_value(self._theme, "frame_padx", 8)),
|
||||||
|
pady=(0, int(theme_value(self._theme, "frame_pady", 8))),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
mid.configure(fg_color=theme_color(self._theme, "mid_frame_fg_color", ("#e5e5e5", "#383838")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
mid.grid_columnconfigure(0, weight=1)
|
mid.grid_columnconfigure(0, weight=1)
|
||||||
mid.grid_rowconfigure(0, weight=1)
|
mid.grid_rowconfigure(0, weight=1)
|
||||||
|
|
||||||
self.tree = ttk.Treeview(mid, columns=("Ubicazione", "NumUDC"), show="headings", selectmode="browse")
|
self.tree = ttk.Treeview(
|
||||||
|
mid,
|
||||||
|
columns=("Ubicazione", "NumUDC"),
|
||||||
|
show="headings",
|
||||||
|
selectmode="browse",
|
||||||
|
style="ResetCorsie.Treeview",
|
||||||
|
)
|
||||||
self.tree.heading("Ubicazione", text="Ubicazione")
|
self.tree.heading("Ubicazione", text="Ubicazione")
|
||||||
self.tree.heading("NumUDC", text="UDC in cella")
|
self.tree.heading("NumUDC", text="UDC in cella")
|
||||||
self.tree.column("Ubicazione", width=240, anchor="w")
|
self.tree.column(
|
||||||
self.tree.column("NumUDC", width=120, anchor="e")
|
"Ubicazione",
|
||||||
|
width=int(theme_value(self._theme, "tree_col_ubicazione_width", 340)),
|
||||||
|
anchor=str(theme_value(self._theme, "tree_col_ubicazione_anchor", "center")),
|
||||||
|
)
|
||||||
|
self.tree.column(
|
||||||
|
"NumUDC",
|
||||||
|
width=int(theme_value(self._theme, "tree_col_num_udc_width", 160)),
|
||||||
|
anchor=str(theme_value(self._theme, "tree_col_num_udc_anchor", "center")),
|
||||||
|
)
|
||||||
|
self.tree.tag_configure("odd", background=theme_value(self._theme, "tree_row_odd_bg", "#ffffff"))
|
||||||
|
self.tree.tag_configure("even", background=theme_value(self._theme, "tree_row_even_bg", "#f3f6fb"))
|
||||||
|
|
||||||
sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview)
|
sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview)
|
||||||
sx = ttk.Scrollbar(mid, orient="horizontal", command=self.tree.xview)
|
sx = ttk.Scrollbar(mid, orient="horizontal", command=self.tree.xview)
|
||||||
@@ -138,11 +422,27 @@ class ResetCorsieWindow(ctk.CTkToplevel):
|
|||||||
sx.grid(row=1, column=0, sticky="ew")
|
sx.grid(row=1, column=0, sticky="ew")
|
||||||
|
|
||||||
bottom = ctk.CTkFrame(self)
|
bottom = ctk.CTkFrame(self)
|
||||||
bottom.pack(fill="x", padx=8, pady=(0, 8))
|
bottom.pack(
|
||||||
ctk.CTkLabel(bottom, text="Riepilogo", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=8, pady=(8, 0))
|
fill="x",
|
||||||
|
padx=int(theme_value(self._theme, "frame_padx", 8)),
|
||||||
|
pady=(0, int(theme_value(self._theme, "frame_pady", 8))),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
bottom.configure(fg_color=theme_color(self._theme, "bottom_frame_fg_color", ("#dcdcdc", "#363636")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ctk.CTkLabel(
|
||||||
|
bottom,
|
||||||
|
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))
|
||||||
|
|
||||||
g = ctk.CTkFrame(bottom)
|
g = ctk.CTkFrame(bottom)
|
||||||
g.pack(fill="x", padx=8, pady=8)
|
g.pack(fill="x", padx=8, pady=8)
|
||||||
|
try:
|
||||||
|
g.configure(fg_color=theme_color(self._theme, "inner_summary_frame_fg_color", ("#d4d4d4", "#404040")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.var_tot_celle = tk.StringVar(value="0")
|
self.var_tot_celle = tk.StringVar(value="0")
|
||||||
self.var_occ = tk.StringVar(value="0")
|
self.var_occ = tk.StringVar(value="0")
|
||||||
self.var_dbl = tk.StringVar(value="0")
|
self.var_dbl = tk.StringVar(value="0")
|
||||||
@@ -150,8 +450,16 @@ class ResetCorsieWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
def _kv(parent_widget, label, var, col):
|
def _kv(parent_widget, label, var, col):
|
||||||
"""Build a compact summary label/value pair."""
|
"""Build a compact summary label/value pair."""
|
||||||
ctk.CTkLabel(parent_widget, text=label, font=("Segoe UI", 9, "bold")).grid(row=0, column=col * 2, sticky="w", padx=(0, 6))
|
ctk.CTkLabel(
|
||||||
ctk.CTkLabel(parent_widget, textvariable=var).grid(row=0, column=col * 2 + 1, sticky="w", padx=(0, 18))
|
parent_widget,
|
||||||
|
text=label,
|
||||||
|
font=theme_font(self._theme, "summary_label_font", ("Segoe UI", 9, "bold")),
|
||||||
|
).grid(row=0, column=col * 2, sticky="w", padx=(0, 6))
|
||||||
|
ctk.CTkLabel(
|
||||||
|
parent_widget,
|
||||||
|
textvariable=var,
|
||||||
|
font=theme_font(self._theme, "summary_value_font", ("Segoe UI", 9)),
|
||||||
|
).grid(row=0, column=col * 2 + 1, sticky="w", padx=(0, 18))
|
||||||
|
|
||||||
g.grid_columnconfigure(7, weight=1)
|
g.grid_columnconfigure(7, weight=1)
|
||||||
_kv(g, "Tot. celle:", self.var_tot_celle, 0)
|
_kv(g, "Tot. celle:", self.var_tot_celle, 0)
|
||||||
@@ -159,10 +467,14 @@ class ResetCorsieWindow(ctk.CTkToplevel):
|
|||||||
_kv(g, "Celle doppie:", self.var_dbl, 2)
|
_kv(g, "Celle doppie:", self.var_dbl, 2)
|
||||||
_kv(g, "Tot. pallet:", self.var_pallet, 3)
|
_kv(g, "Tot. pallet:", self.var_pallet, 3)
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def _load_corsie(self):
|
def _load_corsie(self):
|
||||||
"""Load available aisles and preselect ``1A`` when present."""
|
"""Load available aisles and preselect ``1A`` when present."""
|
||||||
|
_log_sql("reset_corsie_corsie", SQL_CORSIE, {})
|
||||||
|
|
||||||
def _ok(res):
|
def _ok(res):
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
_log_dataset("reset_corsie_corsie", rows)
|
||||||
items = [r[0] for r in rows]
|
items = [r[0] for r in rows]
|
||||||
self.cmb.configure(values=items)
|
self.cmb.configure(values=items)
|
||||||
if items:
|
if items:
|
||||||
@@ -173,20 +485,37 @@ class ResetCorsieWindow(ctk.CTkToplevel):
|
|||||||
messagebox.showinfo("Info", "Nessuna corsia trovata.", parent=self)
|
messagebox.showinfo("Info", "Nessuna corsia trovata.", parent=self)
|
||||||
|
|
||||||
def _err(ex):
|
def _err(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore caricamento corsie reset corsie: {ex}")
|
||||||
messagebox.showerror("Errore", f"Caricamento corsie fallito:\n{ex}", parent=self)
|
messagebox.showerror("Errore", f"Caricamento corsie fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
self._async.run(self.db.query_json(SQL_CORSIE, {}), _ok, _err, busy=self._busy, message="Carico corsie...")
|
self._async.run(self.db.query_json(SQL_CORSIE, {}), _ok, _err, busy=self._busy, message="Carico corsie...")
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh both the summary counters and the occupied-cell list."""
|
"""Refresh both the summary counters and the occupied-cell list."""
|
||||||
corsia = self.cmb.get().strip()
|
corsia = self.cmb.get().strip()
|
||||||
if not corsia:
|
if not corsia:
|
||||||
return
|
return
|
||||||
|
_log_sql("reset_corsie_riepilogo", SQL_RIEPILOGO, {"corsia": corsia})
|
||||||
|
_log_sql("reset_corsie_dettaglio", SQL_DETTAGLIO, {"corsia": corsia})
|
||||||
|
|
||||||
def _ok_sum(res):
|
async def _q():
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
riepilogo = await self.db.query_json(SQL_RIEPILOGO, {"corsia": corsia})
|
||||||
if rows:
|
dettaglio = await self.db.query_json(SQL_DETTAGLIO, {"corsia": corsia})
|
||||||
tot, occ, dbl, pallet = rows[0]
|
return {"riepilogo": riepilogo, "dettaglio": dettaglio}
|
||||||
|
|
||||||
|
def _ok(payload):
|
||||||
|
try:
|
||||||
|
riepilogo = payload.get("riepilogo", {}) if isinstance(payload, dict) else {}
|
||||||
|
dettaglio = payload.get("dettaglio", {}) if isinstance(payload, dict) else {}
|
||||||
|
|
||||||
|
sum_rows = riepilogo.get("rows", []) if isinstance(riepilogo, dict) else []
|
||||||
|
det_rows = dettaglio.get("rows", []) if isinstance(dettaglio, dict) else []
|
||||||
|
_log_dataset("reset_corsie_riepilogo", sum_rows)
|
||||||
|
_log_dataset("reset_corsie_dettaglio", det_rows)
|
||||||
|
|
||||||
|
if sum_rows:
|
||||||
|
tot, occ, dbl, pallet = sum_rows[0]
|
||||||
self.var_tot_celle.set(str(tot or 0))
|
self.var_tot_celle.set(str(tot or 0))
|
||||||
self.var_occ.set(str(occ or 0))
|
self.var_occ.set(str(occ or 0))
|
||||||
self.var_dbl.set(str(dbl or 0))
|
self.var_dbl.set(str(dbl or 0))
|
||||||
@@ -197,68 +526,150 @@ class ResetCorsieWindow(ctk.CTkToplevel):
|
|||||||
self.var_dbl.set("0")
|
self.var_dbl.set("0")
|
||||||
self.var_pallet.set("0")
|
self.var_pallet.set("0")
|
||||||
|
|
||||||
def _err_sum(ex):
|
|
||||||
messagebox.showerror("Errore", f"Riepilogo fallito:\n{ex}", parent=self)
|
|
||||||
|
|
||||||
self._async.run(self.db.query_json(SQL_RIEPILOGO, {"corsia": corsia}), _ok_sum, _err_sum, busy=self._busy, message=f"Riepilogo {corsia}...")
|
|
||||||
|
|
||||||
def _ok_det(res):
|
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
|
||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
self.tree.delete(item)
|
self.tree.delete(item)
|
||||||
for _idc, ubi, n in rows:
|
for idx, (_idc, ubi, n) in enumerate(det_rows):
|
||||||
self.tree.insert("", "end", values=(ubi, n))
|
tag = "even" if idx % 2 else "odd"
|
||||||
|
self.tree.insert("", "end", values=(ubi, n), tags=(tag,))
|
||||||
|
except Exception as ex:
|
||||||
|
_MODULE_LOGGER.exception(f"Errore UI refresh reset corsie corsia={corsia}: {ex}")
|
||||||
|
messagebox.showerror("Errore", f"Aggiornamento interfaccia fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
def _err_det(ex):
|
def _err(ex):
|
||||||
messagebox.showerror("Errore", f"Dettaglio fallito:\n{ex}", parent=self)
|
_MODULE_LOGGER.exception(f"Errore refresh reset corsie corsia={corsia}: {ex}")
|
||||||
|
messagebox.showerror("Errore", f"Refresh fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
self._async.run(self.db.query_json(SQL_DETTAGLIO, {"corsia": corsia}), _ok_det, _err_det, busy=None, message=None)
|
self._async.run(_q(), _ok, _err, busy=self._busy, message=f"Riepilogo {corsia}...")
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def _ask_reset(self):
|
def _ask_reset(self):
|
||||||
"""Ask for confirmation and start the delete flow for the selected aisle."""
|
"""Ask for confirmation and start the logical unload flow for the selected aisle."""
|
||||||
corsia = self.cmb.get().strip()
|
corsia = self.cmb.get().strip()
|
||||||
if not corsia:
|
if not corsia:
|
||||||
return
|
return
|
||||||
|
_log_sql("reset_corsie_count_reset", SQL_COUNT_RESET, {"corsia": corsia})
|
||||||
|
|
||||||
def _ok_count(res):
|
def _ok_count(res):
|
||||||
rows = res.get("rows", []) if isinstance(res, dict) else []
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
n = int(rows[0][0]) if rows else 0
|
_log_dataset("reset_corsie_count_reset", rows)
|
||||||
if n <= 0:
|
tot_udc = int(rows[0][0] or 0) if rows else 0
|
||||||
messagebox.showinfo("Svuota corsia", f"Nessun pallet da rimuovere per la corsia {corsia}.", parent=self)
|
tot_celle = int(rows[0][1] or 0) if rows else 0
|
||||||
|
if tot_udc <= 0:
|
||||||
|
messagebox.showinfo("Svuota corsia", f"Nessuna UDC attiva da scaricare per la corsia {corsia}.", parent=self)
|
||||||
return
|
return
|
||||||
msg = (
|
msg = (
|
||||||
f"Verranno cancellati {n} record da MagazziniPallet per la corsia {corsia}.",
|
f"Verranno scaricate logicamente {tot_udc} UDC attive distribuite su {tot_celle} celle della corsia {corsia}.",
|
||||||
"Questa operazione e' irreversibile.",
|
"L'operazione verra' eseguita come scarico verso 9000000 / 9999, senza cancellazioni fisiche dirette.",
|
||||||
"Digitare il nome della corsia per confermare:",
|
"Digitare il nome della corsia per confermare:",
|
||||||
)
|
)
|
||||||
confirm = simpledialog.askstring("Conferma", "\n".join(msg), parent=self)
|
confirm = simpledialog.askstring("Conferma", "\n".join(msg), parent=self)
|
||||||
if confirm is None:
|
if confirm is None:
|
||||||
|
_MODULE_LOGGER.info(f"Reset corsia {corsia}: conferma annullata dall'utente")
|
||||||
return
|
return
|
||||||
if confirm.strip().upper() != corsia.upper():
|
if confirm.strip().upper() != corsia.upper():
|
||||||
|
_MODULE_LOGGER.info(f"Reset corsia {corsia}: testo conferma non corrispondente ({confirm!r})")
|
||||||
messagebox.showwarning("Annullato", "Testo di conferma non corrispondente.", parent=self)
|
messagebox.showwarning("Annullato", "Testo di conferma non corrispondente.", parent=self)
|
||||||
return
|
return
|
||||||
self._do_reset(corsia)
|
self._do_reset(corsia)
|
||||||
|
|
||||||
def _err_count(ex):
|
def _err_count(ex):
|
||||||
messagebox.showerror("Errore", f"Conteggio righe da cancellare fallito:\n{ex}", parent=self)
|
_MODULE_LOGGER.exception(f"Errore conteggio reset corsie corsia={corsia}: {ex}")
|
||||||
|
messagebox.showerror("Errore", f"Conteggio UDC da scaricare fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
self._async.run(self.db.query_json(SQL_COUNT_DELETE, {"corsia": corsia}), _ok_count, _err_count, busy=self._busy, message="Verifico...")
|
self._async.run(self.db.query_json(SQL_COUNT_RESET, {"corsia": corsia}), _ok_count, _err_count, busy=self._busy, message="Verifico...")
|
||||||
|
|
||||||
|
@_log_call()
|
||||||
def _do_reset(self, corsia: str):
|
def _do_reset(self, corsia: str):
|
||||||
"""Execute the actual delete and refresh the window afterwards."""
|
"""Execute the logical unload of every active UDC in the selected aisle."""
|
||||||
def _ok_del(_):
|
_log_sql("reset_corsie_udc_reset", SQL_UDC_RESET, {"corsia": corsia})
|
||||||
messagebox.showinfo("Completato", f"Corsia {corsia}: svuotamento completato.", parent=self)
|
|
||||||
|
async def _q():
|
||||||
|
payload = await self.db.query_json(SQL_UDC_RESET, {"corsia": corsia})
|
||||||
|
rows = payload.get("rows", []) if isinstance(payload, dict) else []
|
||||||
|
_log_dataset("reset_corsie_udc_reset", rows)
|
||||||
|
|
||||||
|
success = 0
|
||||||
|
failed: list[dict[str, Any]] = []
|
||||||
|
utente = str(getattr(self.session, "login", "") or "warehouse_ui").strip()
|
||||||
|
|
||||||
|
for barcode_pallet, idcella, ubicazione in rows:
|
||||||
|
try:
|
||||||
|
await move_pallet_async(
|
||||||
|
self.db,
|
||||||
|
barcode_pallet=str(barcode_pallet or "").strip(),
|
||||||
|
target_idcella=9999,
|
||||||
|
target_barcode_cella="9000000",
|
||||||
|
utente=utente,
|
||||||
|
)
|
||||||
|
success += 1
|
||||||
|
except Exception as ex:
|
||||||
|
failed.append(
|
||||||
|
{
|
||||||
|
"barcode_pallet": str(barcode_pallet or ""),
|
||||||
|
"idcella": int(idcella or 0),
|
||||||
|
"ubicazione": str(ubicazione or ""),
|
||||||
|
"error": str(ex),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(rows),
|
||||||
|
"success": success,
|
||||||
|
"failed": failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _ok_del(result):
|
||||||
|
total = int((result or {}).get("total", 0))
|
||||||
|
success = int((result or {}).get("success", 0))
|
||||||
|
failed = list((result or {}).get("failed", []))
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
f"Reset corsia {corsia}: scarico logico completato success={success} total={total} failed={len(failed)}"
|
||||||
|
)
|
||||||
|
if failed:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Completato con errori",
|
||||||
|
(
|
||||||
|
f"Corsia {corsia}: scaricate {success} UDC su {total}.\n"
|
||||||
|
f"Errori su {len(failed)} UDC. Controllare {MODULE_LOG_PATH.name}."
|
||||||
|
),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Completato",
|
||||||
|
f"Corsia {corsia}: svuotamento logico completato su {success} UDC.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def _err_del(ex):
|
def _err_del(ex):
|
||||||
|
_MODULE_LOGGER.exception(f"Errore reset logico corsie corsia={corsia}: {ex}")
|
||||||
messagebox.showerror("Errore", f"Svuotamento fallito:\n{ex}", parent=self)
|
messagebox.showerror("Errore", f"Svuotamento fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
self._async.run(self.db.query_json(SQL_DELETE, {"corsia": corsia}), _ok_del, _err_del, busy=self._busy, message=f"Svuoto {corsia}...")
|
self._async.run(_q(), _ok_del, _err_del, busy=self._busy, message=f"Svuoto {corsia}...")
|
||||||
|
|
||||||
|
|
||||||
def open_reset_corsie_window(parent, db_app):
|
def open_reset_corsie_window(parent, db_app, session=None):
|
||||||
"""Create, focus and return the aisle reset window."""
|
"""Create, focus and return the aisle reset window."""
|
||||||
win = ResetCorsieWindow(parent, db_app)
|
key = "_reset_corsie_window_singleton"
|
||||||
|
ex = getattr(parent, key, None)
|
||||||
|
if ex and ex.winfo_exists():
|
||||||
|
try:
|
||||||
|
ex.deiconify()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ex.lift()
|
||||||
|
ex.focus_force()
|
||||||
|
return ex
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
win = ResetCorsieWindow(parent, db_app, session=session)
|
||||||
|
setattr(parent, key, win)
|
||||||
|
place_window_fullsize_below_parent_later(parent, win)
|
||||||
|
try:
|
||||||
win.lift()
|
win.lift()
|
||||||
win.focus_set()
|
win.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return win
|
return win
|
||||||
|
|||||||
24
rollback_online_history_forms_patch.sql
Normal file
24
rollback_online_history_forms_patch.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Rollback online - form storiche Python
|
||||||
|
|
||||||
|
Rimuove solo le viste Python-only usate dalla form "Storico Picking List".
|
||||||
|
Non tocca oggetti C# legacy e non tocca dati.
|
||||||
|
|
||||||
|
Nota:
|
||||||
|
- la form "Storico movimenti UDC" non ha oggetti dedicati da rimuovere.
|
||||||
|
*/
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingListStorico', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_XMag_ViewPackingListStorico;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_vPreparaPackingList', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_vPreparaPackingList;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_vPreparaPackingListSAMA1', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_vPreparaPackingListSAMA1;
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT 'Rollback form storiche Python completato.';
|
||||||
|
GO
|
||||||
69
rollback_plist_reservation_patch.sql
Normal file
69
rollback_plist_reservation_patch.sql
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rollback della patch prenotazione plist.
|
||||||
|
|
||||||
|
Ripristina dal backup salvato da:
|
||||||
|
- apply_plist_reservation_patch.sql
|
||||||
|
|
||||||
|
Oggetti ripristinati:
|
||||||
|
- dbo.XMag_ViewPackingList
|
||||||
|
- dbo.sp_xExePackingListPallet
|
||||||
|
- dbo.sp_xExePackingListPalletPrenota
|
||||||
|
- dbo.sp_ControllaPrenotazionePackingListPalletNew
|
||||||
|
|
||||||
|
La tabella dbo.PickingListReservation NON viene rimossa:
|
||||||
|
- può restare nel DB
|
||||||
|
- il codice legacy non la usa
|
||||||
|
*/
|
||||||
|
|
||||||
|
DECLARE @BackupTag varchar(64) = 'plist_reservation_fix_alpha2';
|
||||||
|
DECLARE @Sql nvarchar(max);
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.WarehouseObjectBackup', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
RAISERROR('dbo.WarehouseObjectBackup non esiste. Impossibile fare rollback.', 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.WarehouseObjectBackup
|
||||||
|
WHERE BackupTag = @BackupTag
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
RAISERROR('Backup tag %s non trovato. Impossibile fare rollback.', 16, 1, @BackupTag);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT @Sql = Definition
|
||||||
|
FROM dbo.WarehouseObjectBackup
|
||||||
|
WHERE BackupTag = @BackupTag
|
||||||
|
AND ObjectName = N'dbo.XMag_ViewPackingList';
|
||||||
|
|
||||||
|
IF @Sql IS NOT NULL EXEC(@Sql);
|
||||||
|
|
||||||
|
SELECT @Sql = Definition
|
||||||
|
FROM dbo.WarehouseObjectBackup
|
||||||
|
WHERE BackupTag = @BackupTag
|
||||||
|
AND ObjectName = N'dbo.sp_xExePackingListPallet';
|
||||||
|
|
||||||
|
IF @Sql IS NOT NULL EXEC(@Sql);
|
||||||
|
|
||||||
|
SELECT @Sql = Definition
|
||||||
|
FROM dbo.WarehouseObjectBackup
|
||||||
|
WHERE BackupTag = @BackupTag
|
||||||
|
AND ObjectName = N'dbo.sp_xExePackingListPalletPrenota';
|
||||||
|
|
||||||
|
IF @Sql IS NOT NULL EXEC(@Sql);
|
||||||
|
|
||||||
|
SELECT @Sql = Definition
|
||||||
|
FROM dbo.WarehouseObjectBackup
|
||||||
|
WHERE BackupTag = @BackupTag
|
||||||
|
AND ObjectName = N'dbo.sp_ControllaPrenotazionePackingListPalletNew';
|
||||||
|
|
||||||
|
IF @Sql IS NOT NULL EXEC(@Sql);
|
||||||
|
|
||||||
35
rollback_python_parallel_pickinglist_patch.sql
Normal file
35
rollback_python_parallel_pickinglist_patch.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rollback della patch parallela Python.
|
||||||
|
|
||||||
|
Non ripristina oggetti legacy, perche' la patch parallela non li modifica.
|
||||||
|
Rimuove solo gli oggetti dbo.py_* e la tabella di prenotazione Python.
|
||||||
|
*/
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_sp_ControllaPrenotazionePackingListPalletNew', N'P') IS NOT NULL
|
||||||
|
DROP PROCEDURE dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_sp_xExePackingListPalletPrenota', N'P') IS NOT NULL
|
||||||
|
DROP PROCEDURE dbo.py_sp_xExePackingListPalletPrenota;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_sp_xExePackingListPallet', N'P') IS NOT NULL
|
||||||
|
DROP PROCEDURE dbo.py_sp_xExePackingListPallet;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_ViewPackingListRestante', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_ViewPackingListRestante;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingList', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_XMag_ViewPackingList;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.PyPickingListReservation', N'U') IS NOT NULL
|
||||||
|
DROP TABLE dbo.PyPickingListReservation;
|
||||||
|
GO
|
||||||
20
rollback_python_pickinglist_history_views.sql
Normal file
20
rollback_python_pickinglist_history_views.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
Rollback SQL - rimozione viste storiche picking list Python.
|
||||||
|
|
||||||
|
Non tocca oggetti C# legacy.
|
||||||
|
*/
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_XMag_ViewPackingListStorico', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_XMag_ViewPackingListStorico;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_vPreparaPackingList', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_vPreparaPackingList;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.py_vPreparaPackingListSAMA1', N'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.py_vPreparaPackingListSAMA1;
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT 'Viste storiche Python rimosse.';
|
||||||
|
GO
|
||||||
68
runtime_support.py
Normal file
68
runtime_support.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""Runtime helpers for console-less Windows launches."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, TypeVar
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
FATAL_LOG = BASE_DIR / "warehouse_fatal.log"
|
||||||
|
_STDIO_HANDLES = []
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_stdio(app_name: str) -> None:
|
||||||
|
"""Give ``pythonw`` a real stdout/stderr target before loggers are imported."""
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d")
|
||||||
|
if sys.stdout is None:
|
||||||
|
handle = (BASE_DIR / f"{app_name}_stdout_{stamp}.log").open("a", encoding="utf-8", buffering=1)
|
||||||
|
_STDIO_HANDLES.append(handle)
|
||||||
|
sys.stdout = handle
|
||||||
|
if sys.stderr is None:
|
||||||
|
handle = (BASE_DIR / f"{app_name}_stderr_{stamp}.log").open("a", encoding="utf-8", buffering=1)
|
||||||
|
_STDIO_HANDLES.append(handle)
|
||||||
|
sys.stderr = handle
|
||||||
|
|
||||||
|
|
||||||
|
def log_fatal(app_name: str, exc: BaseException) -> None:
|
||||||
|
"""Write one startup/runtime crash to a persistent log file."""
|
||||||
|
|
||||||
|
with FATAL_LOG.open("a", encoding="utf-8") as handle:
|
||||||
|
handle.write("\n" + "=" * 80 + "\n")
|
||||||
|
handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}\n")
|
||||||
|
handle.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
||||||
|
|
||||||
|
|
||||||
|
def show_fatal_message(app_name: str, exc: BaseException) -> None:
|
||||||
|
"""Show a best-effort message box for console-less launches."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
messagebox.showerror(
|
||||||
|
app_name,
|
||||||
|
f"Avvio non riuscito.\n\nDettaglio salvato in:\n{FATAL_LOG}\n\n{exc}",
|
||||||
|
parent=root,
|
||||||
|
)
|
||||||
|
root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def run_with_fatal_log(app_name: str, func: Callable[[], T]) -> T:
|
||||||
|
"""Run an app entry point and persist otherwise invisible ``pythonw`` crashes."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except BaseException as exc:
|
||||||
|
log_fatal(app_name, exc)
|
||||||
|
show_fatal_message(app_name, exc)
|
||||||
|
raise
|
||||||
@@ -7,7 +7,12 @@ from tkinter import filedialog, messagebox, ttk
|
|||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
from gestione_aree_frame_async 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:
|
try:
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
@@ -74,16 +79,25 @@ ORDER BY
|
|||||||
class SearchWindow(ctk.CTkToplevel):
|
class SearchWindow(ctk.CTkToplevel):
|
||||||
"""Window that searches pallets by barcode, lot or product code."""
|
"""Window that searches pallets by barcode, lot or product code."""
|
||||||
|
|
||||||
def __init__(self, parent: tk.Widget, db_app):
|
def __init__(self, parent: tk.Widget, db_app, session=None):
|
||||||
"""Initialize widgets and keep a reference to the shared DB client."""
|
"""Initialize widgets and keep a reference to the shared DB client."""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title("Warehouse - Ricerca UDC/Lotto/Codice")
|
self._theme = theme_section("search_window", {})
|
||||||
self.geometry("1100x720")
|
self._locale_catalog = load_locale_catalog()
|
||||||
self.minsize(900, 560)
|
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.resizable(True, True)
|
||||||
|
|
||||||
self.db = db_app
|
self.db = db_app
|
||||||
self._busy = BusyOverlay(self)
|
self.session = session
|
||||||
|
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._async = AsyncRunner(self)
|
||||||
self._sort_state: dict[str, bool] = {}
|
self._sort_state: dict[str, bool] = {}
|
||||||
|
|
||||||
@@ -95,26 +109,62 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
self.grid_rowconfigure(1, weight=1)
|
self.grid_rowconfigure(1, weight=1)
|
||||||
self.grid_columnconfigure(0, 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)
|
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
top.grid_columnconfigure(i, weight=0)
|
top.grid_columnconfigure(i, weight=0)
|
||||||
top.grid_columnconfigure(7, weight=1)
|
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()
|
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()
|
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()
|
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")
|
btn_search = ctk.CTkButton(
|
||||||
ctk.CTkButton(top, text="Esporta XLSX", command=self._export_xlsx).grid(row=0, column=7, sticky="e")
|
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 = ctk.CTkFrame(self)
|
||||||
wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
||||||
@@ -166,10 +216,22 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
"""Export the currently visible search results to an Excel file."""
|
"""Export the currently visible search results to an Excel file."""
|
||||||
rows = [self.tree.item(iid, "values") for iid in self.tree.get_children("")]
|
rows = [self.tree.item(iid, "values") for iid in self.tree.get_children("")]
|
||||||
if not rows:
|
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
|
return
|
||||||
if not _HAS_XLSX:
|
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
|
return
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -209,9 +271,17 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
for j, width in widths.items():
|
for j, width in widths.items():
|
||||||
ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60)
|
ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60)
|
||||||
wb.save(fname)
|
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:
|
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):
|
def _on_dclick(self, evt):
|
||||||
"""Copy the selected pallet barcode when a result cell is double-clicked."""
|
"""Copy the selected pallet barcode when a result cell is double-clicked."""
|
||||||
@@ -355,8 +425,12 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
if not (udc or lotto or codice):
|
if not (udc or lotto or codice):
|
||||||
if not messagebox.askyesno(
|
if not messagebox.askyesno(
|
||||||
"Conferma",
|
loc_text("search.msg.confirm_title", catalog=self._locale_catalog, default="Conferma"),
|
||||||
"Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?",
|
loc_text(
|
||||||
|
"search.msg.confirm_all",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?",
|
||||||
|
),
|
||||||
parent=self,
|
parent=self,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
@@ -394,8 +468,12 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Nessun risultato",
|
loc_text("search.msg.no_results_title", catalog=self._locale_catalog, default="Nessun risultato"),
|
||||||
"Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
loc_text(
|
||||||
|
"search.msg.no_results",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
||||||
|
),
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -406,12 +484,22 @@ class SearchWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
def _err(ex):
|
def _err(ex):
|
||||||
self._busy.hide()
|
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):
|
def open_search_window(parent, db_app, session=None):
|
||||||
"""Open a singleton-like search window tied to the launcher instance."""
|
"""Open a singleton-like search window tied to the launcher instance."""
|
||||||
key = "_search_window_singleton"
|
key = "_search_window_singleton"
|
||||||
ex = getattr(parent, key, None)
|
ex = getattr(parent, key, None)
|
||||||
@@ -422,6 +510,12 @@ def open_search_window(parent, db_app):
|
|||||||
return ex
|
return ex
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
w = SearchWindow(parent, db_app)
|
w = SearchWindow(parent, db_app, session=session)
|
||||||
setattr(parent, key, w)
|
setattr(parent, key, w)
|
||||||
|
place_window_fullsize_below_parent_later(parent, w)
|
||||||
|
try:
|
||||||
|
w.lift()
|
||||||
|
w.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return w
|
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
|
||||||
|
}
|
||||||
68
specifica_storico_pickinglist.md
Normal file
68
specifica_storico_pickinglist.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Specifica - Storico Picking List
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
La finestra "Storico Picking List" deve permettere di consultare le picking list visibili nel modello dati Python parallelo, comprese quelle gia' esaurite, mostrando lo stato sintetico della lista e il dettaglio delle UDC.
|
||||||
|
|
||||||
|
## Accesso
|
||||||
|
|
||||||
|
- La finestra si apre dal launcher principale tramite il pulsante "Storico Picking List".
|
||||||
|
- In una fase successiva potra' essere collegata alla form "Gestione Picking List" per aprire direttamente il dettaglio della lista selezionata.
|
||||||
|
- La finestra e' disponibile agli operatori autenticati secondo il permesso `launcher.open_history_pickinglist`.
|
||||||
|
|
||||||
|
## Comportamento UI
|
||||||
|
|
||||||
|
- La finestra usa lo stesso posizionamento delle altre form del backoffice.
|
||||||
|
- Tutte le interrogazioni al database sono asincrone.
|
||||||
|
- Durante il caricamento della lista e del dettaglio deve comparire l'overlay standard dell'applicazione.
|
||||||
|
- La parte alta mostra l'elenco delle picking list.
|
||||||
|
- La parte bassa mostra il dettaglio della picking list selezionata.
|
||||||
|
- Il filtro principale e' il numero documento, con ricerca parziale.
|
||||||
|
|
||||||
|
## Logica dati
|
||||||
|
|
||||||
|
La form usa un ramo di lettura storico Python separato dalle viste operative C#.
|
||||||
|
|
||||||
|
Oggetti SQL dedicati:
|
||||||
|
|
||||||
|
- `dbo.py_vPreparaPackingListSAMA1`
|
||||||
|
- `dbo.py_vPreparaPackingList`
|
||||||
|
- `dbo.py_XMag_ViewPackingListStorico`
|
||||||
|
|
||||||
|
Questi oggetti sono creati dallo script `apply_python_pickinglist_history_views.sql` e possono essere rimossi con `rollback_python_pickinglist_history_views.sql`.
|
||||||
|
|
||||||
|
La vista storica non usa `dbo.vPreparaPackingListSAMA1`, `dbo.vPreparaPackingList` o `dbo.XMag_ViewPackingList`, cosi' il programma C# continua a lavorare sul proprio ramo legacy.
|
||||||
|
|
||||||
|
La vista `dbo.py_vPreparaPackingListSAMA1` include:
|
||||||
|
|
||||||
|
- documenti dell'anno corrente: `BAMTES.ANNDOC >= YEAR(GETDATE())`
|
||||||
|
- documenti aperti e chiusi: `BAMTES.STATO IN ('P', 'D')`
|
||||||
|
- nessun filtro sugli ultimi 10 giorni
|
||||||
|
|
||||||
|
La lista alta aggrega per documento:
|
||||||
|
|
||||||
|
- data documento
|
||||||
|
- stato documento gestionale
|
||||||
|
- totale UDC
|
||||||
|
- righe residue
|
||||||
|
- righe spedite
|
||||||
|
- stato operativo
|
||||||
|
- stato prenotazione Python
|
||||||
|
|
||||||
|
Lo stato operativo e' calcolato cosi':
|
||||||
|
|
||||||
|
- `Chiusa`: il documento gestionale ha `BAMTES.STATO = 'D'`.
|
||||||
|
- `Da lavorare`: nessuna riga risulta spedita.
|
||||||
|
- `In corso`: almeno una riga risulta spedita e almeno una riga resta da lavorare.
|
||||||
|
- `Esaurita`: non restano righe operative.
|
||||||
|
|
||||||
|
Il criterio primario per riconoscere una picking list lavorata nello storico e' lo stato gestionale `BAMTES.STATO`.
|
||||||
|
La cella `9999 = 7G.1.1` resta un indizio logistico importante, ma non e' sufficiente come unico criterio storico perche' una UDC non piu' in giacenza puo' ricadere nei fallback della vista.
|
||||||
|
|
||||||
|
## Convenzioni operative
|
||||||
|
|
||||||
|
Una riga con `Cella = 9999` viene considerata spedita/prelevata, coerentemente con la convenzione `7G.1.1` osservata sul barcode.
|
||||||
|
|
||||||
|
## Limiti noti
|
||||||
|
|
||||||
|
Questa finestra e' una prima vista storica di consultazione. Non sostituisce ancora una vera storicizzazione annuale progettata a database. Quando il database verra' riprogettato per FlyWMS, sara' preferibile costruire una vista storica dedicata, con date di chiusura lista, utente, tempi e movimento UDC collegato.
|
||||||
53
specifica_storico_udc.md
Normal file
53
specifica_storico_udc.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Specifica - Storico movimenti UDC
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
La finestra "Storico movimenti UDC" deve permettere di ricostruire, in sola lettura, la sequenza dei movimenti registrati per una UDC. La funzione nasce come strumento di diagnosi per capire dove e da chi una unita' di carico e' stata movimentata fino alla spedizione.
|
||||||
|
|
||||||
|
## Accesso
|
||||||
|
|
||||||
|
- La finestra si apre dal launcher principale tramite il pulsante "Storico movimenti UDC".
|
||||||
|
- In una fase successiva potra' essere richiamata anche dalle form esistenti, ad esempio dalla ricerca UDC o dal dettaglio picking list, passando direttamente il codice UDC selezionato.
|
||||||
|
- La finestra e' disponibile agli operatori autenticati secondo il permesso `launcher.open_history_udc`.
|
||||||
|
|
||||||
|
## Comportamento UI
|
||||||
|
|
||||||
|
- La finestra usa lo stesso posizionamento delle altre form del backoffice.
|
||||||
|
- Tutte le interrogazioni al database sono asincrone.
|
||||||
|
- Durante il caricamento deve comparire l'overlay standard dell'applicazione.
|
||||||
|
- Il filtro principale e' il codice UDC, con ricerca parziale.
|
||||||
|
- La griglia mostra al massimo 500 righe per evitare carichi eccessivi durante i test.
|
||||||
|
- Le righe di tipo `V` sono evidenziate come versamenti/carichi.
|
||||||
|
- Le righe di tipo `P` sono evidenziate come prelievi/scarichi.
|
||||||
|
|
||||||
|
## Dati visualizzati
|
||||||
|
|
||||||
|
La prima implementazione legge da `dbo.MagazziniPallet`, collegando quando possibile `dbo.Celle` per mostrare l'ubicazione in forma leggibile.
|
||||||
|
|
||||||
|
La ricerca non si limita al match diretto su `MagazziniPallet.Attributo`: include anche i movimenti collegati tramite la catena `ID` / `IDRiferimento`. Questo e' importante nel vecchio modello dati, perche' un prelievo o un trasferimento puo' essere legato alla riga originaria di versamento.
|
||||||
|
|
||||||
|
La griglia integra anche la vista `dbo.XMag_GiacenzaPalletPlistChiuse` come fallback gestionale:
|
||||||
|
|
||||||
|
- prima vengono mostrati i movimenti fisici reali `V` e `P` da `dbo.MagazziniPallet`
|
||||||
|
- se per la UDC esiste almeno un movimento fisico `P`, anche collegato tramite la catena `ID` / `IDRiferimento`, non viene aggiunta nessuna riga diagnostica
|
||||||
|
- se non esistono movimenti `P` ma la UDC compare in una picking list gestionale chiusa, viene mostrata una riga diagnostica `SPED`
|
||||||
|
|
||||||
|
La riga `SPED` non e' un movimento fisico: serve solo a spiegare perche' le form operative considerano la UDC gia' spedita anche quando manca la tracciatura fisica `P`. Deve quindi essere considerata l'ultima opzione: se esistono movimenti reali `V` o `P`, questi restano la fonte primaria per data, utente e ubicazione.
|
||||||
|
|
||||||
|
Campi principali:
|
||||||
|
|
||||||
|
- ID movimento
|
||||||
|
- Tipo movimento
|
||||||
|
- Riferimento
|
||||||
|
- UDC
|
||||||
|
- IDCella
|
||||||
|
- Ubicazione
|
||||||
|
- DataMagazzino
|
||||||
|
- Utente di inserimento
|
||||||
|
- Data inserimento
|
||||||
|
- Utente di modifica
|
||||||
|
- Data modifica
|
||||||
|
|
||||||
|
## Limiti noti
|
||||||
|
|
||||||
|
La funzione e' diagnostica e non modifica dati. Se in futuro servira' uno storico piu' ricco, sara' opportuno introdurre una vista o tabella storica dedicata con nomi parlanti nel nuovo schema FlyWMS.
|
||||||
364
storico_pickinglist.py
Normal file
364
storico_pickinglist.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
"""Read-only picking-list history window."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox, ttk
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
from busy_overlay import InlineBusyOverlay
|
||||||
|
from gestione_aree import AsyncRunner
|
||||||
|
from locale_text import load_locale_catalog, text as loc_text
|
||||||
|
from ui_theme import theme_color, theme_font, theme_section, theme_value
|
||||||
|
from window_placement import place_window_fullsize_below_parent_later
|
||||||
|
|
||||||
|
|
||||||
|
SQL_STORICO_PL = """
|
||||||
|
WITH base AS (
|
||||||
|
SELECT *
|
||||||
|
FROM dbo.py_XMag_ViewPackingListStorico
|
||||||
|
),
|
||||||
|
agg AS (
|
||||||
|
SELECT
|
||||||
|
Documento,
|
||||||
|
MAX(DataDocumento) AS DataDocumento,
|
||||||
|
MAX(StatoDocumento) AS StatoDocumento,
|
||||||
|
MAX(NAZIONE) AS NAZIONE,
|
||||||
|
MAX(CodNazione) AS CodNazione,
|
||||||
|
COUNT(DISTINCT Pallet) AS TotUDC,
|
||||||
|
SUM(CASE WHEN Cella = 9999 THEN 1 ELSE 0 END) AS RigheSpedite,
|
||||||
|
SUM(CASE WHEN Cella <> 9999 OR Cella IS NULL THEN 1 ELSE 0 END) AS RigheResidue,
|
||||||
|
COUNT(*) AS RigheTotali,
|
||||||
|
MIN(Ordinamento) AS PrimoOrdine,
|
||||||
|
MAX(IDStato) AS IDStato
|
||||||
|
FROM base
|
||||||
|
GROUP BY Documento
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
Documento,
|
||||||
|
DataDocumento,
|
||||||
|
StatoDocumento,
|
||||||
|
NAZIONE,
|
||||||
|
CodNazione,
|
||||||
|
TotUDC,
|
||||||
|
RigheResidue,
|
||||||
|
RigheSpedite,
|
||||||
|
RigheTotali,
|
||||||
|
CASE
|
||||||
|
WHEN StatoDocumento = 'D' THEN 'Chiusa'
|
||||||
|
WHEN RigheResidue = 0 THEN 'Esaurita'
|
||||||
|
WHEN RigheSpedite > 0 THEN 'In corso'
|
||||||
|
ELSE 'Da lavorare'
|
||||||
|
END AS StatoOperativo,
|
||||||
|
IDStato,
|
||||||
|
PrimoOrdine
|
||||||
|
FROM agg
|
||||||
|
WHERE (:documento IS NULL OR CAST(Documento AS varchar(32)) LIKE CONCAT('%', :documento, '%'))
|
||||||
|
ORDER BY Documento DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_STORICO_PL_DETAILS = """
|
||||||
|
SELECT
|
||||||
|
Documento,
|
||||||
|
Pallet,
|
||||||
|
Lotto,
|
||||||
|
Articolo,
|
||||||
|
Descrizione,
|
||||||
|
Qta,
|
||||||
|
DataDocumento,
|
||||||
|
StatoDocumento,
|
||||||
|
Cella,
|
||||||
|
Ubicazione,
|
||||||
|
Ordinamento,
|
||||||
|
IDStato
|
||||||
|
FROM dbo.py_XMag_ViewPackingListStorico
|
||||||
|
WHERE Documento = :documento
|
||||||
|
ORDER BY Ordinamento, Pallet;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(value: Any) -> str:
|
||||||
|
"""Format SQL Server/SAMA date values into dd/mm/yyyy for operators."""
|
||||||
|
|
||||||
|
if value in (None, ""):
|
||||||
|
return ""
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.strftime("%d/%m/%Y")
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value.strftime("%d/%m/%Y")
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
try:
|
||||||
|
# SQL Server numeric datetime: CAST(datetime AS int), day 0 = 1900-01-01.
|
||||||
|
return (datetime(1900, 1, 1) + timedelta(days=int(value))).strftime("%d/%m/%Y")
|
||||||
|
except Exception:
|
||||||
|
return str(value)
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
if text.isdigit() and len(text) == 8:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(text, "%Y%m%d").strftime("%d/%m/%Y")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(text.replace("Z", "+00:00")).strftime("%d/%m/%Y")
|
||||||
|
except Exception:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class StoricoPickingListWindow(ctk.CTkToplevel):
|
||||||
|
"""Window that shows historical picking-list status and details."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Widget, db_client, session=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.db_client = db_client
|
||||||
|
self.session = session
|
||||||
|
self._theme = theme_section("history_picking_window", theme_section("pickinglist_window", {}))
|
||||||
|
self._locale_catalog = load_locale_catalog()
|
||||||
|
self._async = AsyncRunner(self)
|
||||||
|
self._busy = InlineBusyOverlay(self, self._theme)
|
||||||
|
self.var_documento = tk.StringVar()
|
||||||
|
|
||||||
|
self.title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"))
|
||||||
|
self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720")))
|
||||||
|
minsize = theme_value(self._theme, "window_minsize", [980, 560])
|
||||||
|
self.minsize(int(minsize[0]), int(minsize[1]))
|
||||||
|
try:
|
||||||
|
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self.after(300, self._load_master)
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
self.grid_rowconfigure(1, weight=1)
|
||||||
|
self.grid_rowconfigure(3, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
top = ctk.CTkFrame(
|
||||||
|
self,
|
||||||
|
fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")),
|
||||||
|
)
|
||||||
|
top.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
|
||||||
|
top.grid_columnconfigure(3, weight=1)
|
||||||
|
|
||||||
|
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="Documento:", font=label_font).grid(row=0, column=0, sticky="w")
|
||||||
|
ctk.CTkEntry(top, textvariable=self.var_documento, width=140, font=entry_font).grid(
|
||||||
|
row=0, column=1, sticky="w", padx=(4, 12)
|
||||||
|
)
|
||||||
|
ctk.CTkButton(
|
||||||
|
top,
|
||||||
|
text=loc_text("history.picking.button.reload", catalog=self._locale_catalog, default="Ricarica"),
|
||||||
|
command=self._load_master,
|
||||||
|
font=button_font,
|
||||||
|
).grid(
|
||||||
|
row=0, column=2, sticky="w"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.master_tree = self._make_tree(
|
||||||
|
row=1,
|
||||||
|
columns=("Documento", "Data", "StatoDoc", "NAZIONE", "TotUDC", "Residue", "Spedite", "Stato", "IDStato"),
|
||||||
|
widths={
|
||||||
|
"Documento": 100,
|
||||||
|
"Data": 110,
|
||||||
|
"StatoDoc": 75,
|
||||||
|
"NAZIONE": 260,
|
||||||
|
"TotUDC": 90,
|
||||||
|
"Residue": 90,
|
||||||
|
"Spedite": 90,
|
||||||
|
"Stato": 120,
|
||||||
|
"IDStato": 80,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.master_tree.bind("<<TreeviewSelect>>", self._on_master_select)
|
||||||
|
|
||||||
|
ctk.CTkLabel(
|
||||||
|
self,
|
||||||
|
text=loc_text("history.picking.detail_title", catalog=self._locale_catalog, default="Dettaglio contenuto"),
|
||||||
|
anchor="w",
|
||||||
|
font=button_font,
|
||||||
|
).grid(
|
||||||
|
row=2, column=0, sticky="ew", padx=8, pady=(4, 2)
|
||||||
|
)
|
||||||
|
self.detail_tree = self._make_tree(
|
||||||
|
row=3,
|
||||||
|
columns=("Pallet", "Lotto", "Articolo", "Descrizione", "Qta", "Data", "StatoDoc", "Cella", "Ubicazione", "Ordine"),
|
||||||
|
widths={
|
||||||
|
"Pallet": 120,
|
||||||
|
"Lotto": 120,
|
||||||
|
"Articolo": 130,
|
||||||
|
"Descrizione": 320,
|
||||||
|
"Qta": 80,
|
||||||
|
"Data": 110,
|
||||||
|
"StatoDoc": 75,
|
||||||
|
"Cella": 80,
|
||||||
|
"Ubicazione": 180,
|
||||||
|
"Ordine": 80,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_tree(self, *, row: int, columns: tuple[str, ...], widths: dict[str, int]) -> ttk.Treeview:
|
||||||
|
wrap = ctk.CTkFrame(self)
|
||||||
|
wrap.grid(row=row, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
||||||
|
wrap.grid_rowconfigure(0, weight=1)
|
||||||
|
wrap.grid_columnconfigure(0, weight=1)
|
||||||
|
tree = ttk.Treeview(wrap, columns=columns, show="headings")
|
||||||
|
for col in columns:
|
||||||
|
tree.heading(col, text=col)
|
||||||
|
tree.column(col, width=widths.get(col, 100), anchor="w")
|
||||||
|
tree.tag_configure("done", background="#ECECEC")
|
||||||
|
tree.tag_configure("active", background="#EAF7EA")
|
||||||
|
sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview)
|
||||||
|
sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview)
|
||||||
|
tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
||||||
|
tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sy.grid(row=0, column=1, sticky="ns")
|
||||||
|
sx.grid(row=1, column=0, sticky="ew")
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def _load_master(self) -> None:
|
||||||
|
params = {"documento": str(self.var_documento.get() or "").strip() or None}
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await self.db_client.query_json(SQL_STORICO_PL, params)
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
self._fill_master(_rows_to_dicts(res))
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
messagebox.showerror(
|
||||||
|
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||||
|
loc_text(
|
||||||
|
"history.picking.msg.load_error",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Errore caricamento:\n{error}",
|
||||||
|
).format(error=ex),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._async.run(
|
||||||
|
_job(),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
busy=self._busy,
|
||||||
|
message=loc_text(
|
||||||
|
"history.picking.busy.master",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Carico storico picking list...",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fill_master(self, rows: list[dict[str, Any]]) -> None:
|
||||||
|
self.master_tree.delete(*self.master_tree.get_children(""))
|
||||||
|
self.detail_tree.delete(*self.detail_tree.get_children(""))
|
||||||
|
for index, row in enumerate(rows):
|
||||||
|
stato = str(row.get("StatoOperativo") or "")
|
||||||
|
tag = "done" if stato in {"Chiusa", "Esaurita"} else "active" if int(row.get("IDStato") or 0) == 1 else ""
|
||||||
|
self.master_tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
iid=f"doc_{row.get('Documento')}_{index}",
|
||||||
|
values=(
|
||||||
|
row.get("Documento", ""),
|
||||||
|
_format_date(row.get("DataDocumento", "")),
|
||||||
|
row.get("StatoDocumento", ""),
|
||||||
|
row.get("NAZIONE", ""),
|
||||||
|
row.get("TotUDC", ""),
|
||||||
|
row.get("RigheResidue", ""),
|
||||||
|
row.get("RigheSpedite", ""),
|
||||||
|
stato,
|
||||||
|
row.get("IDStato", ""),
|
||||||
|
),
|
||||||
|
tags=(tag,) if tag else (),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_master_select(self, _event=None) -> None:
|
||||||
|
selected = self.master_tree.selection()
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
values = self.master_tree.item(selected[0], "values")
|
||||||
|
if not values:
|
||||||
|
return
|
||||||
|
documento = values[0]
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento})
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
self._fill_detail(_rows_to_dicts(res))
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
messagebox.showerror(
|
||||||
|
loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"),
|
||||||
|
loc_text(
|
||||||
|
"history.picking.msg.detail_error",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Errore dettaglio:\n{error}",
|
||||||
|
).format(error=ex),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._async.run(
|
||||||
|
_job(),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
busy=self._busy,
|
||||||
|
message=loc_text(
|
||||||
|
"history.picking.busy.detail",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Carico dettaglio picking list...",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fill_detail(self, rows: list[dict[str, Any]]) -> None:
|
||||||
|
self.detail_tree.delete(*self.detail_tree.get_children(""))
|
||||||
|
for row in rows:
|
||||||
|
done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999
|
||||||
|
self.detail_tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
values=(
|
||||||
|
row.get("Pallet", ""),
|
||||||
|
row.get("Lotto", ""),
|
||||||
|
row.get("Articolo", ""),
|
||||||
|
row.get("Descrizione", ""),
|
||||||
|
row.get("Qta", ""),
|
||||||
|
_format_date(row.get("DataDocumento", "")),
|
||||||
|
row.get("StatoDocumento", ""),
|
||||||
|
row.get("Cella", ""),
|
||||||
|
row.get("Ubicazione", ""),
|
||||||
|
row.get("Ordinamento", ""),
|
||||||
|
),
|
||||||
|
tags=("done",) if done else (),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc:
|
||||||
|
"""Open the picking-list history window."""
|
||||||
|
|
||||||
|
win = StoricoPickingListWindow(parent, db_client, session=session)
|
||||||
|
place_window_fullsize_below_parent_later(parent, win)
|
||||||
|
win.bind("<Escape>", lambda _e: win.destroy())
|
||||||
|
return win
|
||||||
312
storico_udc.py
Normal file
312
storico_udc.py
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
"""Read-only UDC movement history window."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox, ttk
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
from busy_overlay import InlineBusyOverlay
|
||||||
|
from gestione_aree import AsyncRunner
|
||||||
|
from locale_text import load_locale_catalog, text as loc_text
|
||||||
|
from ui_theme import theme_color, theme_font, theme_section, theme_value
|
||||||
|
from window_placement import place_window_fullsize_below_parent_later
|
||||||
|
|
||||||
|
|
||||||
|
SQL_STORICO_UDC = """
|
||||||
|
WITH direct AS (
|
||||||
|
SELECT
|
||||||
|
ID,
|
||||||
|
IDRiferimento
|
||||||
|
FROM dbo.MagazziniPallet
|
||||||
|
WHERE (:udc IS NULL OR Attributo COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%'))
|
||||||
|
),
|
||||||
|
shipped AS (
|
||||||
|
SELECT
|
||||||
|
shipped.BarcodePallet,
|
||||||
|
shipped.NumeroPallet,
|
||||||
|
shipped.IDMagazzino,
|
||||||
|
shipped.IDArea,
|
||||||
|
shipped.IDCella
|
||||||
|
FROM dbo.XMag_GiacenzaPalletPlistChiuse shipped
|
||||||
|
WHERE (:udc IS NULL OR shipped.BarcodePallet COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%'))
|
||||||
|
),
|
||||||
|
has_physical_p AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
s.BarcodePallet
|
||||||
|
FROM shipped s
|
||||||
|
JOIN dbo.MagazziniPallet seed
|
||||||
|
ON seed.Attributo COLLATE Latin1_General_CI_AS =
|
||||||
|
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
JOIN dbo.MagazziniPallet mp
|
||||||
|
ON mp.Tipo = 'P'
|
||||||
|
AND (
|
||||||
|
mp.Attributo COLLATE Latin1_General_CI_AS =
|
||||||
|
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
OR mp.ID = seed.ID
|
||||||
|
OR mp.IDRiferimento = seed.ID
|
||||||
|
OR (seed.IDRiferimento IS NOT NULL AND seed.IDRiferimento > 0 AND mp.ID = seed.IDRiferimento)
|
||||||
|
OR (seed.IDRiferimento IS NOT NULL AND seed.IDRiferimento > 0 AND mp.IDRiferimento = seed.IDRiferimento)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
roots AS (
|
||||||
|
SELECT ID AS RootID
|
||||||
|
FROM direct
|
||||||
|
UNION
|
||||||
|
SELECT IDRiferimento AS RootID
|
||||||
|
FROM direct
|
||||||
|
WHERE IDRiferimento IS NOT NULL AND IDRiferimento > 0
|
||||||
|
UNION
|
||||||
|
SELECT mp.ID AS RootID
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN shipped s
|
||||||
|
ON s.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
mp.Attributo COLLATE Latin1_General_CI_AS
|
||||||
|
)
|
||||||
|
SELECT TOP (500) *
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
CAST(mp.ID AS int) AS ID,
|
||||||
|
CAST(mp.Tipo AS varchar(8)) AS Tipo,
|
||||||
|
CAST(mp.IDRiferimento AS int) AS IDRiferimento,
|
||||||
|
CAST(mp.NumeroPallet AS int) AS NumeroPallet,
|
||||||
|
CAST(mp.Attributo AS varchar(16)) AS UDC,
|
||||||
|
CAST(mp.IDMagazzino AS int) AS IDMagazzino,
|
||||||
|
CAST(mp.IDArea AS int) AS IDArea,
|
||||||
|
CAST(mp.IDCella AS int) AS IDCella,
|
||||||
|
UPPER(
|
||||||
|
CONCAT(
|
||||||
|
COALESCE(LTRIM(RTRIM(c.Corsia)), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(c.Colonna AS varchar(32)))), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(c.Fila AS varchar(32)))), 'NA')
|
||||||
|
)
|
||||||
|
) AS Ubicazione,
|
||||||
|
CAST(mp.DataMagazzino AS datetime) AS DataMagazzino,
|
||||||
|
CAST(mp.InsUtente AS varchar(50)) AS InsUtente,
|
||||||
|
CAST(mp.InsDataOra AS datetime) AS InsDataOra,
|
||||||
|
CAST(mp.ModUtente AS varchar(50)) AS ModUtente,
|
||||||
|
CAST(mp.ModDataOra AS datetime) AS ModDataOra
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
LEFT JOIN dbo.Celle c ON c.ID = mp.IDCella
|
||||||
|
WHERE
|
||||||
|
:udc IS NULL
|
||||||
|
OR mp.Attributo COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%')
|
||||||
|
OR mp.ID IN (SELECT RootID FROM roots)
|
||||||
|
OR mp.IDRiferimento IN (SELECT RootID FROM roots)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
CAST(NULL AS int) AS ID,
|
||||||
|
CAST('SPED' AS varchar(8)) AS Tipo,
|
||||||
|
CAST(NULL AS int) AS IDRiferimento,
|
||||||
|
CAST(s.NumeroPallet AS int) AS NumeroPallet,
|
||||||
|
CAST(s.BarcodePallet AS varchar(16)) AS UDC,
|
||||||
|
CAST(s.IDMagazzino AS int) AS IDMagazzino,
|
||||||
|
CAST(s.IDArea AS int) AS IDArea,
|
||||||
|
CAST(s.IDCella AS int) AS IDCella,
|
||||||
|
UPPER(
|
||||||
|
CONCAT(
|
||||||
|
COALESCE(LTRIM(RTRIM(c.Corsia)), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(c.Colonna AS varchar(32)))), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(c.Fila AS varchar(32)))), 'NA')
|
||||||
|
)
|
||||||
|
) AS Ubicazione,
|
||||||
|
CAST(NULL AS datetime) AS DataMagazzino,
|
||||||
|
CAST('Picking list chiusa' AS varchar(50)) AS InsUtente,
|
||||||
|
CAST(NULL AS datetime) AS InsDataOra,
|
||||||
|
CAST(NULL AS varchar(50)) AS ModUtente,
|
||||||
|
CAST(NULL AS datetime) AS ModDataOra
|
||||||
|
FROM shipped s
|
||||||
|
LEFT JOIN dbo.Celle c ON c.ID = s.IDCella
|
||||||
|
LEFT JOIN has_physical_p hp
|
||||||
|
ON hp.BarcodePallet COLLATE Latin1_General_CI_AS =
|
||||||
|
s.BarcodePallet COLLATE Latin1_General_CI_AS
|
||||||
|
WHERE hp.BarcodePallet IS NULL
|
||||||
|
) rows
|
||||||
|
ORDER BY UDC, CASE WHEN ID IS NULL THEN 0 ELSE 1 END DESC, ID DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class StoricoUDCWindow(ctk.CTkToplevel):
|
||||||
|
"""Window that shows the P/V movement timeline of one or more UDCs."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Widget, db_client, session=None, initial_udc: str | None = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.db_client = db_client
|
||||||
|
self.session = session
|
||||||
|
self._theme = theme_section("history_udc_window", theme_section("search_window", {}))
|
||||||
|
self._locale_catalog = load_locale_catalog()
|
||||||
|
self._async = AsyncRunner(self)
|
||||||
|
self._busy = InlineBusyOverlay(self, self._theme)
|
||||||
|
self.var_udc = tk.StringVar(value=str(initial_udc or ""))
|
||||||
|
|
||||||
|
self.title(loc_text("history.udc.title", catalog=self._locale_catalog, default="Storico movimenti UDC"))
|
||||||
|
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]))
|
||||||
|
try:
|
||||||
|
self.configure(fg_color=theme_color(self._theme, "window_fg_color", ("#efefef", "#2f2f2f")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
if initial_udc:
|
||||||
|
self.after(250, self._do_search)
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
self.grid_rowconfigure(1, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
top = ctk.CTkFrame(
|
||||||
|
self,
|
||||||
|
fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")),
|
||||||
|
)
|
||||||
|
top.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
|
||||||
|
top.grid_columnconfigure(3, weight=1)
|
||||||
|
|
||||||
|
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="UDC:", font=label_font).grid(row=0, column=0, sticky="w")
|
||||||
|
ctk.CTkEntry(top, textvariable=self.var_udc, width=160, font=entry_font).grid(
|
||||||
|
row=0, column=1, sticky="w", padx=(4, 12)
|
||||||
|
)
|
||||||
|
ctk.CTkButton(
|
||||||
|
top,
|
||||||
|
text=loc_text("history.udc.button.search", catalog=self._locale_catalog, default="Cerca"),
|
||||||
|
command=self._do_search,
|
||||||
|
font=button_font,
|
||||||
|
).grid(
|
||||||
|
row=0, column=2, sticky="w"
|
||||||
|
)
|
||||||
|
|
||||||
|
wrap = ctk.CTkFrame(self)
|
||||||
|
wrap.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
|
||||||
|
wrap.grid_rowconfigure(0, weight=1)
|
||||||
|
wrap.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
cols = (
|
||||||
|
"ID",
|
||||||
|
"Tipo",
|
||||||
|
"Rif",
|
||||||
|
"UDC",
|
||||||
|
"Cella",
|
||||||
|
"Ubicazione",
|
||||||
|
"Data",
|
||||||
|
"InsUtente",
|
||||||
|
"InsDataOra",
|
||||||
|
"ModUtente",
|
||||||
|
"ModDataOra",
|
||||||
|
)
|
||||||
|
self.tree = ttk.Treeview(wrap, columns=cols, show="headings")
|
||||||
|
for col in cols:
|
||||||
|
self.tree.heading(col, text=col)
|
||||||
|
self.tree.column(col, width=90, anchor="w")
|
||||||
|
self.tree.column("ID", width=70, anchor="e")
|
||||||
|
self.tree.column("Tipo", width=55, anchor="center")
|
||||||
|
self.tree.column("Rif", width=70, anchor="e")
|
||||||
|
self.tree.column("UDC", width=110)
|
||||||
|
self.tree.column("Cella", width=70, anchor="e")
|
||||||
|
self.tree.column("Ubicazione", width=130)
|
||||||
|
self.tree.column("Data", width=150)
|
||||||
|
self.tree.column("InsDataOra", width=150)
|
||||||
|
self.tree.column("ModDataOra", width=150)
|
||||||
|
self.tree.tag_configure("versamento", background="#EAF7EA")
|
||||||
|
self.tree.tag_configure("prelievo", background="#FFF0E6")
|
||||||
|
self.tree.tag_configure("spedita", background="#FFECEC", foreground="#B00020")
|
||||||
|
|
||||||
|
sy = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
|
||||||
|
sx = ttk.Scrollbar(wrap, orient="horizontal", command=self.tree.xview)
|
||||||
|
self.tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
||||||
|
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sy.grid(row=0, column=1, sticky="ns")
|
||||||
|
sx.grid(row=1, column=0, sticky="ew")
|
||||||
|
|
||||||
|
def _do_search(self) -> None:
|
||||||
|
udc = str(self.var_udc.get() or "").strip()
|
||||||
|
params = {"udc": udc or None}
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await self.db_client.query_json(SQL_STORICO_UDC, params)
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
rows = _rows_to_dicts(res)
|
||||||
|
self._fill(rows)
|
||||||
|
if not rows:
|
||||||
|
messagebox.showinfo(
|
||||||
|
loc_text("history.udc.msg.title", catalog=self._locale_catalog, default="Storico UDC"),
|
||||||
|
loc_text("history.udc.msg.empty", catalog=self._locale_catalog, default="Nessun movimento trovato."),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
messagebox.showerror(
|
||||||
|
loc_text("history.udc.msg.title", catalog=self._locale_catalog, default="Storico UDC"),
|
||||||
|
loc_text(
|
||||||
|
"history.udc.msg.error",
|
||||||
|
catalog=self._locale_catalog,
|
||||||
|
default="Errore ricerca:\n{error}",
|
||||||
|
).format(error=ex),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._async.run(
|
||||||
|
_job(),
|
||||||
|
_ok,
|
||||||
|
_err,
|
||||||
|
busy=self._busy,
|
||||||
|
message=loc_text("history.udc.busy", catalog=self._locale_catalog, default="Carico storico UDC..."),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fill(self, rows: list[dict[str, Any]]) -> None:
|
||||||
|
self.tree.delete(*self.tree.get_children(""))
|
||||||
|
def _value(row: dict[str, Any], name: str) -> Any:
|
||||||
|
value = row.get(name, "")
|
||||||
|
return "" if value is None else value
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
tipo = str(row.get("Tipo") or "")
|
||||||
|
tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else ""
|
||||||
|
self.tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
values=(
|
||||||
|
_value(row, "ID"),
|
||||||
|
tipo,
|
||||||
|
_value(row, "IDRiferimento"),
|
||||||
|
_value(row, "UDC"),
|
||||||
|
_value(row, "IDCella"),
|
||||||
|
_value(row, "Ubicazione"),
|
||||||
|
_value(row, "DataMagazzino"),
|
||||||
|
_value(row, "InsUtente"),
|
||||||
|
_value(row, "InsDataOra"),
|
||||||
|
_value(row, "ModUtente"),
|
||||||
|
_value(row, "ModDataOra"),
|
||||||
|
),
|
||||||
|
tags=(tag,) if tag else (),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def open_storico_udc_window(parent: tk.Misc, db_client, session=None, initial_udc: str | None = None) -> tk.Misc:
|
||||||
|
"""Open the UDC history window."""
|
||||||
|
|
||||||
|
win = StoricoUDCWindow(parent, db_client, session=session, initial_udc=initial_udc)
|
||||||
|
place_window_fullsize_below_parent_later(parent, win)
|
||||||
|
win.bind("<Escape>", lambda _e: win.destroy())
|
||||||
|
return win
|
||||||
71
tooltip.json
Normal file
71
tooltip.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"default_language": "IT",
|
||||||
|
"IT": {
|
||||||
|
"launcher.open_reset_corsie": "Apre la finestra di gestione corsie per visualizzare il contenuto di una corsia e svuotarla in modo controllato.",
|
||||||
|
"launcher.open_layout": "Apre la vista layout delle corsie con celle, UDC presenti e menu operativo contestuale.",
|
||||||
|
"launcher.open_multi_udc": "Apre la vista UDC fantasma per analizzare celle con piu' pallet e bonificare i candidati fantasma.",
|
||||||
|
"launcher.open_search": "Apre la ricerca UDC per trovare rapidamente una unita' di carico e verificarne la posizione.",
|
||||||
|
"launcher.open_history_udc": "Apre lo storico movimenti UDC per ricostruire carichi, scarichi, celle e utenti coinvolti.",
|
||||||
|
"launcher.open_pickinglist": "Apre la gestione delle picking list per prenotare, controllare e aggiornare le liste di prelievo.",
|
||||||
|
"launcher.open_history_pickinglist": "Apre lo storico picking list per consultare liste, stato operativo e dettaglio UDC.",
|
||||||
|
"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.",
|
||||||
|
"layout.refresh": "Ricarica la corsia selezionata e aggiorna matrice, colori e statistiche.",
|
||||||
|
"layout.export_xlsx": "Esporta la vista corrente del layout corsia in un file Excel.",
|
||||||
|
"multi_udc.refresh": "Ricarica l'albero delle celle con UDC multiple e il riepilogo percentuale per corsia.",
|
||||||
|
"multi_udc.expand_all": "Espande tutti i nodi gia' caricati nell'albero.",
|
||||||
|
"multi_udc.collapse_all": "Comprime tutti i nodi dell'albero.",
|
||||||
|
"multi_udc.preselect": "Espande la corsia selezionata e preseleziona automaticamente le UDC con causale fantasma.",
|
||||||
|
"multi_udc.remove_ghosts": "Scarica logicamente le UDC selezionate della corsia attiva verso l'ubicazione di uscita.",
|
||||||
|
"multi_udc.export_xlsx": "Esporta in Excel il contenuto corrente della vista UDC fantasma."
|
||||||
|
},
|
||||||
|
"ENG": {
|
||||||
|
"launcher.open_reset_corsie": "Open the aisle management window to inspect an aisle and empty it in a controlled way.",
|
||||||
|
"launcher.open_layout": "Open the aisle layout view with cells, present UDCs and the operational context menu.",
|
||||||
|
"launcher.open_multi_udc": "Open the ghost UDC view to inspect cells with multiple pallets and clean ghost candidates.",
|
||||||
|
"launcher.open_search": "Open the UDC search window to quickly locate a load unit and verify its position.",
|
||||||
|
"launcher.open_history_udc": "Open UDC movement history to review loads, unloads, cells and users involved.",
|
||||||
|
"launcher.open_pickinglist": "Open picking list management to reserve, inspect and update picking lists.",
|
||||||
|
"launcher.open_history_pickinglist": "Open picking-list history to inspect lists, operational status and UDC detail.",
|
||||||
|
"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.",
|
||||||
|
"layout.refresh": "Reload the selected aisle and refresh matrix, colors and statistics.",
|
||||||
|
"layout.export_xlsx": "Export the current aisle layout view to an Excel file.",
|
||||||
|
"multi_udc.refresh": "Reload the tree of cells with multiple UDCs and the percentage summary by aisle.",
|
||||||
|
"multi_udc.expand_all": "Expand all nodes currently loaded in the tree.",
|
||||||
|
"multi_udc.collapse_all": "Collapse all tree nodes.",
|
||||||
|
"multi_udc.preselect": "Expand the selected aisle and automatically preselect UDCs classified as ghost candidates.",
|
||||||
|
"multi_udc.remove_ghosts": "Logically unload the selected UDCs of the active aisle to the outbound location.",
|
||||||
|
"multi_udc.export_xlsx": "Export the current ghost UDC view to Excel."
|
||||||
|
}
|
||||||
|
}
|
||||||
101
tooltips.py
Normal file
101
tooltips.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""Tooltip catalog and widget helper utilities."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
|
||||||
|
_TOOLTIP_FILE = Path(__file__).with_name("tooltip.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_tooltip_catalog() -> dict:
|
||||||
|
"""Load the tooltip catalog from JSON, returning a safe default on errors."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(_TOOLTIP_FILE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {"default_language": "IT", "IT": {}, "ENG": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def tooltip_text(key: str, *, language: str | None = None, catalog: dict | None = None) -> str:
|
||||||
|
"""Return the localized tooltip text for ``key``."""
|
||||||
|
|
||||||
|
data = catalog or load_tooltip_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 {}
|
||||||
|
return str(fallback.get(key, ""))
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetToolTip:
|
||||||
|
"""Simple delayed tooltip for Tk/customtkinter widgets."""
|
||||||
|
|
||||||
|
def __init__(self, widget: tk.Misc, text: str, *, delay_ms: int = 400, wraplength: int = 320):
|
||||||
|
self.widget = widget
|
||||||
|
self.text = text.strip()
|
||||||
|
self.delay_ms = int(delay_ms)
|
||||||
|
self.wraplength = int(wraplength)
|
||||||
|
self._after_id: str | None = None
|
||||||
|
self._tip: tk.Toplevel | None = None
|
||||||
|
if self.text:
|
||||||
|
self.widget.bind("<Enter>", self._schedule_show, add="+")
|
||||||
|
self.widget.bind("<Leave>", self._hide, add="+")
|
||||||
|
self.widget.bind("<ButtonPress>", self._hide, add="+")
|
||||||
|
|
||||||
|
def _schedule_show(self, _event=None):
|
||||||
|
self._cancel_schedule()
|
||||||
|
self._after_id = self.widget.after(self.delay_ms, self._show)
|
||||||
|
|
||||||
|
def _cancel_schedule(self):
|
||||||
|
if self._after_id is not None:
|
||||||
|
try:
|
||||||
|
self.widget.after_cancel(self._after_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._after_id = None
|
||||||
|
|
||||||
|
def _show(self):
|
||||||
|
self._after_id = None
|
||||||
|
if self._tip is not None or not self.text:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
x = self.widget.winfo_rootx() + 18
|
||||||
|
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 8
|
||||||
|
tip = tk.Toplevel(self.widget)
|
||||||
|
tip.withdraw()
|
||||||
|
tip.overrideredirect(True)
|
||||||
|
tip.attributes("-topmost", True)
|
||||||
|
frame = tk.Frame(tip, bg="#fff7c7", bd=1, relief="solid")
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
label = tk.Label(
|
||||||
|
frame,
|
||||||
|
text=self.text,
|
||||||
|
justify="left",
|
||||||
|
anchor="w",
|
||||||
|
wraplength=self.wraplength,
|
||||||
|
bg="#fff7c7",
|
||||||
|
fg="#1f1f1f",
|
||||||
|
font=("Segoe UI", 9),
|
||||||
|
padx=8,
|
||||||
|
pady=6,
|
||||||
|
)
|
||||||
|
label.pack(fill="both", expand=True)
|
||||||
|
tip.geometry(f"+{x}+{y}")
|
||||||
|
tip.deiconify()
|
||||||
|
self._tip = tip
|
||||||
|
except Exception:
|
||||||
|
self._tip = None
|
||||||
|
|
||||||
|
def _hide(self, _event=None):
|
||||||
|
self._cancel_schedule()
|
||||||
|
tip = self._tip
|
||||||
|
self._tip = None
|
||||||
|
if tip is not None:
|
||||||
|
try:
|
||||||
|
tip.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
336
ui_theme.json
Normal file
336
ui_theme.json
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"window_bg": ["#f1f1f1", "#2b2b2b"],
|
||||||
|
"panel_bg": ["#d9d9d9", "#3a3a3a"],
|
||||||
|
"panel_alt_bg": ["#cfcfcf", "#454545"],
|
||||||
|
"text_primary": "#1f1f1f",
|
||||||
|
"text_secondary": "#4b4b4b",
|
||||||
|
"accent": "#2ebf74",
|
||||||
|
"accent_hover": "#28a766",
|
||||||
|
"danger": "#ca3d3d",
|
||||||
|
"danger_hover": "#aa2f2f",
|
||||||
|
"border": "#bdbdbd"
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"window_width": 1280,
|
||||||
|
"window_min_width": 960,
|
||||||
|
"window_top_x": 0,
|
||||||
|
"window_top_y": 0,
|
||||||
|
"outer_pady": 10,
|
||||||
|
"outer_padx": 0,
|
||||||
|
"max_buttons_per_row": 7,
|
||||||
|
"button_padx": 6,
|
||||||
|
"button_pady": 6,
|
||||||
|
"info_padx": 6,
|
||||||
|
"info_pady": [4, 2],
|
||||||
|
"exit_icon_color": "#ca3d3d",
|
||||||
|
"info_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 12,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"cascade_x_offset": 42,
|
||||||
|
"cascade_y_offset": 34,
|
||||||
|
"cascade_margin_left": 0,
|
||||||
|
"cascade_margin_top": 0,
|
||||||
|
"button_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 11,
|
||||||
|
"weight": "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_corsie": {
|
||||||
|
"window_geometry": "1000x680",
|
||||||
|
"window_minsize": [880, 560],
|
||||||
|
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||||
|
"top_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||||
|
"mid_frame_fg_color": ["#e5e5e5", "#383838"],
|
||||||
|
"bottom_frame_fg_color": ["#dcdcdc", "#363636"],
|
||||||
|
"inner_summary_frame_fg_color": ["#d4d4d4", "#404040"],
|
||||||
|
"frame_padx": 8,
|
||||||
|
"frame_pady": 8,
|
||||||
|
"toolbar_button_width": 140,
|
||||||
|
"toolbar_button_height": 28,
|
||||||
|
"toolbar_button_corner_radius": 6,
|
||||||
|
"toolbar_button_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"toolbar_label_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "normal"
|
||||||
|
},
|
||||||
|
"combobox_width": 140,
|
||||||
|
"combobox_height": 28,
|
||||||
|
"combobox_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "normal"
|
||||||
|
},
|
||||||
|
"summary_title_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 12,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"summary_label_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 9,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"summary_value_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 9,
|
||||||
|
"weight": "normal"
|
||||||
|
},
|
||||||
|
"tree_heading_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"tree_body_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "normal"
|
||||||
|
},
|
||||||
|
"tree_row_height": 30,
|
||||||
|
"tree_heading_bg": "#9fb2cb",
|
||||||
|
"tree_heading_bg_active": "#90a5c0",
|
||||||
|
"tree_heading_fg": "#10243e",
|
||||||
|
"tree_heading_padding": [8, 6],
|
||||||
|
"tree_body_bg": "#ffffff",
|
||||||
|
"tree_body_fg": "#1f1f1f",
|
||||||
|
"tree_row_odd_bg": "#ffffff",
|
||||||
|
"tree_row_even_bg": "#edf3fb",
|
||||||
|
"tree_selected_bg": "#cfe4ff",
|
||||||
|
"tree_selected_fg": "#10243e",
|
||||||
|
"tree_col_ubicazione_width": 360,
|
||||||
|
"tree_col_ubicazione_anchor": "center",
|
||||||
|
"tree_col_num_udc_width": 180,
|
||||||
|
"tree_col_num_udc_anchor": "center",
|
||||||
|
"tree_show_grid_hint": true,
|
||||||
|
"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]
|
||||||
|
},
|
||||||
|
"layout_window": {
|
||||||
|
"window_geometry": "1200x740",
|
||||||
|
"window_minsize": [980, 560],
|
||||||
|
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||||
|
"top_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||||
|
"panel_frame_fg_color": ["#dcdcdc", "#363636"],
|
||||||
|
"toolbar_button_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"toolbar_label_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 12,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"entry_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "normal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"multi_udc": {
|
||||||
|
"window_geometry": "1100x700",
|
||||||
|
"window_minsize": [900, 550],
|
||||||
|
"window_fg_color": ["#efefef", "#2f2f2f"],
|
||||||
|
"toolbar_frame_fg_color": ["#d7d7d7", "#3b3b3b"],
|
||||||
|
"content_frame_fg_color": ["#e5e5e5", "#383838"],
|
||||||
|
"summary_frame_fg_color": ["#dcdcdc", "#363636"],
|
||||||
|
"inner_summary_frame_fg_color": ["#d4d4d4", "#404040"],
|
||||||
|
"toolbar_button_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 10,
|
||||||
|
"weight": "bold"
|
||||||
|
},
|
||||||
|
"summary_title_font": {
|
||||||
|
"family": "Segoe UI",
|
||||||
|
"size": 12,
|
||||||
|
"weight": "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login_window": {
|
||||||
|
"window_geometry": "165x155+0+0",
|
||||||
|
"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]
|
||||||
|
},
|
||||||
|
"history_udc_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]
|
||||||
|
},
|
||||||
|
"history_picking_window": {
|
||||||
|
"window_geometry": "1200x720",
|
||||||
|
"window_minsize": [980, 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]
|
||||||
|
}
|
||||||
|
}
|
||||||
97
ui_theme.py
Normal file
97
ui_theme.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""External UI theme loader for the warehouse desktop application.
|
||||||
|
|
||||||
|
The module reads ``ui_theme.json`` from the workspace root and exposes a few
|
||||||
|
helpers to resolve fonts, colors, paddings and section-specific configuration
|
||||||
|
without hardcoding presentation details inside each window module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
THEME_PATH = Path(__file__).with_name("ui_theme.json")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def load_theme() -> dict[str, Any]:
|
||||||
|
"""Load the external JSON theme, returning an empty dict on failure."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(THEME_PATH.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def reload_theme() -> dict[str, Any]:
|
||||||
|
"""Clear cache and reload the theme from disk."""
|
||||||
|
|
||||||
|
load_theme.cache_clear()
|
||||||
|
return load_theme()
|
||||||
|
|
||||||
|
|
||||||
|
def theme_section(name: str, default: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
"""Return one top-level theme section as a dictionary."""
|
||||||
|
|
||||||
|
theme = load_theme()
|
||||||
|
value = theme.get(name)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
return dict(default or {})
|
||||||
|
|
||||||
|
|
||||||
|
def theme_value(section: dict[str, Any], key: str, default: Any = None) -> Any:
|
||||||
|
"""Return a scalar theme value from a section."""
|
||||||
|
|
||||||
|
return section.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def theme_color(section: dict[str, Any], key: str, default: Any = None) -> Any:
|
||||||
|
"""Return a CTk-compatible color value (string or light/dark tuple)."""
|
||||||
|
|
||||||
|
value = section.get(key, default)
|
||||||
|
if isinstance(value, list) and len(value) == 2:
|
||||||
|
return tuple(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def theme_padding(section: dict[str, Any], key: str, default: tuple[int, ...]) -> tuple[int, ...]:
|
||||||
|
"""Return tuple-like padding values from a JSON list."""
|
||||||
|
|
||||||
|
value = section.get(key)
|
||||||
|
if isinstance(value, list):
|
||||||
|
try:
|
||||||
|
return tuple(int(v) for v in value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def theme_font(section: dict[str, Any], key: str, default: tuple[Any, ...]) -> tuple[Any, ...]:
|
||||||
|
"""Resolve a Tk font tuple from JSON."""
|
||||||
|
|
||||||
|
spec = section.get(key)
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
return default
|
||||||
|
|
||||||
|
family = spec.get("family", default[0] if default else "Segoe UI")
|
||||||
|
size = int(spec.get("size", default[1] if len(default) > 1 else 10))
|
||||||
|
|
||||||
|
parts: list[Any] = [family, size]
|
||||||
|
|
||||||
|
weight = str(spec.get("weight", "")).strip().lower()
|
||||||
|
if weight in {"bold", "normal"} and weight != "normal":
|
||||||
|
parts.append(weight)
|
||||||
|
|
||||||
|
slant = str(spec.get("slant", "")).strip().lower()
|
||||||
|
if slant == "italic":
|
||||||
|
parts.append(slant)
|
||||||
|
|
||||||
|
if bool(spec.get("underline", False)):
|
||||||
|
parts.append("underline")
|
||||||
|
if bool(spec.get("overstrike", False)):
|
||||||
|
parts.append("overstrike")
|
||||||
|
|
||||||
|
return tuple(parts)
|
||||||
94
user_session.py
Normal file
94
user_session.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"""Application user session and permission scaffolding for the warehouse app."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import FrozenSet
|
||||||
|
|
||||||
|
|
||||||
|
ALL_ACTIONS: FrozenSet[str] = frozenset(
|
||||||
|
{
|
||||||
|
"launcher.open_reset_corsie",
|
||||||
|
"launcher.open_layout",
|
||||||
|
"launcher.open_multi_udc",
|
||||||
|
"launcher.open_search",
|
||||||
|
"launcher.open_history_udc",
|
||||||
|
"launcher.open_pickinglist",
|
||||||
|
"launcher.open_history_pickinglist",
|
||||||
|
"launcher.arrange_windows",
|
||||||
|
"launcher.exit",
|
||||||
|
"reset_corsie.view",
|
||||||
|
"search.view",
|
||||||
|
"history_udc.view",
|
||||||
|
"history_pickinglist.view",
|
||||||
|
"multi_udc.view",
|
||||||
|
"layout.view",
|
||||||
|
"layout.carico",
|
||||||
|
"layout.scarico",
|
||||||
|
"layout.prenota",
|
||||||
|
"layout.libera_prenotazione",
|
||||||
|
"layout.disabilita_cella",
|
||||||
|
"layout.abilita_cella",
|
||||||
|
"pickinglist.view",
|
||||||
|
"pickinglist.prenota",
|
||||||
|
"pickinglist.sprenota",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_allowed_actions(_privilegio: int | None) -> FrozenSet[str]:
|
||||||
|
"""Return the currently enabled action set for the given operator profile.
|
||||||
|
|
||||||
|
For this first iteration every authenticated operator can execute every
|
||||||
|
function, but the explicit action map already exists so that a future admin
|
||||||
|
UI can refine profiles without refactoring the whole application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ALL_ACTIONS
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class UserSession:
|
||||||
|
"""Minimal in-memory representation of one authenticated application user."""
|
||||||
|
|
||||||
|
operator_id: int
|
||||||
|
login: str
|
||||||
|
nominativo: str
|
||||||
|
privilegio: int | None = None
|
||||||
|
codice_unita: str = ""
|
||||||
|
session_started_at: datetime = field(default_factory=datetime.now)
|
||||||
|
allowed_actions: FrozenSet[str] = field(default_factory=lambda: ALL_ACTIONS)
|
||||||
|
|
||||||
|
def can(self, action: str) -> bool:
|
||||||
|
"""Return whether the current user can execute one named action."""
|
||||||
|
|
||||||
|
return action in self.allowed_actions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
"""Return the best human-readable identity for the current session."""
|
||||||
|
|
||||||
|
if str(self.nominativo or "").strip():
|
||||||
|
return str(self.nominativo).strip()
|
||||||
|
return str(self.login or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_session(
|
||||||
|
*,
|
||||||
|
operator_id: int,
|
||||||
|
login: str,
|
||||||
|
nominativo: str,
|
||||||
|
privilegio: int | None,
|
||||||
|
codice_unita: str,
|
||||||
|
) -> UserSession:
|
||||||
|
"""Create one user session with the current default action set."""
|
||||||
|
|
||||||
|
return UserSession(
|
||||||
|
operator_id=int(operator_id),
|
||||||
|
login=str(login or "").strip(),
|
||||||
|
nominativo=str(nominativo or "").strip(),
|
||||||
|
privilegio=int(privilegio) if privilegio is not None else None,
|
||||||
|
codice_unita=str(codice_unita or "").strip(),
|
||||||
|
allowed_actions=build_allowed_actions(privilegio),
|
||||||
|
)
|
||||||
1112
view_celle_multi_udc.py
Normal file
1112
view_celle_multi_udc.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,431 +0,0 @@
|
|||||||
"""Exploration window for cells containing more than one pallet."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import tkinter as tk
|
|
||||||
from datetime import datetime
|
|
||||||
from tkinter import filedialog, messagebox, ttk
|
|
||||||
|
|
||||||
import customtkinter as ctk
|
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.styles import Alignment, Font
|
|
||||||
|
|
||||||
from gestione_aree_frame_async import AsyncRunner
|
|
||||||
|
|
||||||
|
|
||||||
def _json_obj(res):
|
|
||||||
"""Normalize raw DB responses into a dictionary with a ``rows`` key."""
|
|
||||||
if isinstance(res, str):
|
|
||||||
try:
|
|
||||||
res = json.loads(res)
|
|
||||||
except Exception as ex:
|
|
||||||
raise RuntimeError(f"Risposta non JSON: {ex}\nRaw: {res!r}")
|
|
||||||
if isinstance(res, dict) and "error" in res:
|
|
||||||
err = res.get("error") or "Errore sconosciuto"
|
|
||||||
detail = res.get("sql") or ""
|
|
||||||
raise RuntimeError(f"{err}\n{detail}")
|
|
||||||
return res if isinstance(res, dict) else {"rows": res}
|
|
||||||
|
|
||||||
|
|
||||||
UBI_B = (
|
|
||||||
"UPPER("
|
|
||||||
" CONCAT("
|
|
||||||
" RTRIM(b.Corsia), '.', RTRIM(CAST(b.Colonna AS varchar(32))), '.', RTRIM(CAST(b.Fila AS varchar(32)))"
|
|
||||||
" )"
|
|
||||||
")"
|
|
||||||
)
|
|
||||||
|
|
||||||
BASE_CTE = """
|
|
||||||
WITH base AS (
|
|
||||||
SELECT
|
|
||||||
g.IDCella,
|
|
||||||
g.BarcodePallet,
|
|
||||||
RTRIM(c.Corsia) AS Corsia,
|
|
||||||
c.Colonna,
|
|
||||||
c.Fila
|
|
||||||
FROM dbo.XMag_GiacenzaPallet AS g
|
|
||||||
JOIN dbo.Celle AS c ON c.ID = g.IDCella
|
|
||||||
WHERE g.IDCella <> 9999 AND RTRIM(c.Corsia) <> '7G'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
SQL_CORSIE = BASE_CTE + """
|
|
||||||
, dup_celle AS (
|
|
||||||
SELECT IDCCella = b.IDCella
|
|
||||||
FROM base b
|
|
||||||
GROUP BY b.IDCella
|
|
||||||
HAVING COUNT(DISTINCT b.BarcodePallet) > 1
|
|
||||||
)
|
|
||||||
SELECT DISTINCT b.Corsia
|
|
||||||
FROM base b
|
|
||||||
WHERE EXISTS (SELECT 1 FROM dup_celle d WHERE d.IDCCella = b.IDCella)
|
|
||||||
ORDER BY b.Corsia;
|
|
||||||
"""
|
|
||||||
|
|
||||||
SQL_CELLE_DUP_PER_CORSIA = BASE_CTE + f"""
|
|
||||||
, dup_celle AS (
|
|
||||||
SELECT b.IDCella, COUNT(DISTINCT b.BarcodePallet) AS NumUDC
|
|
||||||
FROM base b
|
|
||||||
GROUP BY b.IDCella
|
|
||||||
HAVING COUNT(DISTINCT b.BarcodePallet) > 1
|
|
||||||
)
|
|
||||||
SELECT dc.IDCella,
|
|
||||||
{UBI_B} AS Ubicazione,
|
|
||||||
b.Colonna, b.Fila, b.Corsia,
|
|
||||||
dc.NumUDC
|
|
||||||
FROM dup_celle dc
|
|
||||||
JOIN base b ON b.IDCella = dc.IDCella
|
|
||||||
WHERE b.Corsia = RTRIM(:corsia)
|
|
||||||
GROUP BY dc.IDCella, {UBI_B}, b.Colonna, b.Fila, b.Corsia, dc.NumUDC
|
|
||||||
ORDER BY b.Colonna, b.Fila;
|
|
||||||
"""
|
|
||||||
|
|
||||||
SQL_PALLET_IN_CELLA = BASE_CTE + """
|
|
||||||
SELECT
|
|
||||||
b.BarcodePallet AS Pallet,
|
|
||||||
ta.Descrizione,
|
|
||||||
ta.Lotto
|
|
||||||
FROM base b
|
|
||||||
OUTER APPLY (
|
|
||||||
SELECT TOP (1) t.Descrizione, t.Lotto
|
|
||||||
FROM dbo.vXTracciaProdotti AS t
|
|
||||||
WHERE t.Pallet = b.BarcodePallet COLLATE Latin1_General_CI_AS
|
|
||||||
ORDER BY t.Lotto
|
|
||||||
) AS ta
|
|
||||||
WHERE b.IDCella = :idcella
|
|
||||||
GROUP BY b.BarcodePallet, ta.Descrizione, ta.Lotto
|
|
||||||
ORDER BY b.BarcodePallet;
|
|
||||||
"""
|
|
||||||
|
|
||||||
SQL_RIEPILOGO_PERCENTUALI = BASE_CTE + """
|
|
||||||
, tot AS (
|
|
||||||
SELECT b.Corsia, COUNT(DISTINCT b.IDCella) AS TotCelle
|
|
||||||
FROM base b GROUP BY b.Corsia
|
|
||||||
),
|
|
||||||
dup_celle AS (
|
|
||||||
SELECT b.Corsia, b.IDCella
|
|
||||||
FROM base b
|
|
||||||
GROUP BY b.Corsia, b.IDCella
|
|
||||||
HAVING COUNT(DISTINCT b.BarcodePallet) > 1
|
|
||||||
),
|
|
||||||
per_corsia AS (
|
|
||||||
SELECT t.Corsia, t.TotCelle, COALESCE(d.CelleMultiple, 0) AS CelleMultiple
|
|
||||||
FROM tot t
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT Corsia, COUNT(IDCella) AS CelleMultiple
|
|
||||||
FROM dup_celle GROUP BY Corsia
|
|
||||||
) d ON d.Corsia = t.Corsia
|
|
||||||
),
|
|
||||||
unione AS (
|
|
||||||
SELECT Corsia, TotCelle, CelleMultiple,
|
|
||||||
CAST(100.0 * CelleMultiple / NULLIF(TotCelle, 0) AS decimal(5,2)) AS Percentuale,
|
|
||||||
CAST(0 AS int) AS Ord
|
|
||||||
FROM per_corsia
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TOTALE' AS Corsia,
|
|
||||||
SUM(TotCelle), SUM(CelleMultiple),
|
|
||||||
CAST(100.0 * SUM(CelleMultiple) / NULLIF(SUM(TotCelle), 0) AS decimal(5,2)),
|
|
||||||
CAST(1 AS int) AS Ord
|
|
||||||
FROM per_corsia
|
|
||||||
)
|
|
||||||
SELECT Corsia, TotCelle, CelleMultiple, Percentuale
|
|
||||||
FROM unione
|
|
||||||
ORDER BY Ord, Corsia;
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class CelleMultipleWindow(ctk.CTkToplevel):
|
|
||||||
"""Tree-based explorer for duplicated pallet allocations."""
|
|
||||||
|
|
||||||
def __init__(self, root, db_client, runner: AsyncRunner | None = None):
|
|
||||||
"""Bind the shared DB client and immediately load the tree summary."""
|
|
||||||
super().__init__(root)
|
|
||||||
self.title("Celle con piu' pallet")
|
|
||||||
self.geometry("1100x700")
|
|
||||||
self.minsize(900, 550)
|
|
||||||
self.resizable(True, True)
|
|
||||||
|
|
||||||
self.db = db_client
|
|
||||||
self.runner = runner or AsyncRunner(self)
|
|
||||||
|
|
||||||
self._build_layout()
|
|
||||||
self._bind_events()
|
|
||||||
self.refresh_all()
|
|
||||||
|
|
||||||
def _build_layout(self):
|
|
||||||
"""Create the toolbar, lazy-loaded tree and percentage summary table."""
|
|
||||||
self.grid_rowconfigure(0, weight=5)
|
|
||||||
self.grid_rowconfigure(1, weight=70)
|
|
||||||
self.grid_rowconfigure(2, weight=25, minsize=160)
|
|
||||||
self.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
toolbar = ctk.CTkFrame(self)
|
|
||||||
toolbar.grid(row=0, column=0, sticky="nsew")
|
|
||||||
ctk.CTkButton(toolbar, text="Aggiorna", command=self.refresh_all).pack(side="left", padx=6, pady=4)
|
|
||||||
ctk.CTkButton(toolbar, text="Espandi tutto", command=self.expand_all).pack(side="left", padx=6, pady=4)
|
|
||||||
ctk.CTkButton(toolbar, text="Comprimi tutto", command=self.collapse_all).pack(side="left", padx=6, pady=4)
|
|
||||||
ctk.CTkButton(toolbar, text="Esporta in XLSX", command=self.export_to_xlsx).pack(side="left", padx=6, pady=4)
|
|
||||||
|
|
||||||
frame = ctk.CTkFrame(self)
|
|
||||||
frame.grid(row=1, column=0, sticky="nsew", padx=6, pady=(0, 6))
|
|
||||||
frame.grid_rowconfigure(0, weight=1)
|
|
||||||
frame.grid_columnconfigure(0, weight=1)
|
|
||||||
self.tree = ttk.Treeview(frame, columns=("col2", "col3"), show="tree headings", selectmode="browse")
|
|
||||||
self.tree.heading("#0", text="Nodo")
|
|
||||||
self.tree.heading("col2", text="Descrizione")
|
|
||||||
self.tree.heading("col3", text="Lotto")
|
|
||||||
y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
|
||||||
x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
|
||||||
self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set)
|
|
||||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
|
||||||
y.grid(row=0, column=1, sticky="ns")
|
|
||||||
x.grid(row=1, column=0, sticky="ew")
|
|
||||||
|
|
||||||
sumf = ctk.CTkFrame(self)
|
|
||||||
sumf.grid(row=2, column=0, sticky="nsew", padx=6, pady=(0, 6))
|
|
||||||
ctk.CTkLabel(sumf, text="Riepilogo % celle multiple per corsia", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=8, pady=(8, 0))
|
|
||||||
inner = ctk.CTkFrame(sumf)
|
|
||||||
inner.pack(fill="both", expand=True, padx=6, pady=6)
|
|
||||||
inner.grid_rowconfigure(0, weight=1)
|
|
||||||
inner.grid_columnconfigure(0, weight=1)
|
|
||||||
self.sum_tbl = ttk.Treeview(inner, columns=("Corsia", "TotCelle", "CelleMultiple", "Percentuale"), show="headings")
|
|
||||||
for key, title, width, anchor in (
|
|
||||||
("Corsia", "Corsia", 100, "center"),
|
|
||||||
("TotCelle", "Totale celle", 120, "e"),
|
|
||||||
("CelleMultiple", ">1 UDC", 120, "e"),
|
|
||||||
("Percentuale", "%", 80, "e"),
|
|
||||||
):
|
|
||||||
self.sum_tbl.heading(key, text=title)
|
|
||||||
self.sum_tbl.column(key, width=width, anchor=anchor)
|
|
||||||
y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview)
|
|
||||||
x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview)
|
|
||||||
self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set)
|
|
||||||
self.sum_tbl.grid(row=0, column=0, sticky="nsew")
|
|
||||||
y2.grid(row=0, column=1, sticky="ns")
|
|
||||||
x2.grid(row=1, column=0, sticky="ew")
|
|
||||||
|
|
||||||
def _bind_events(self):
|
|
||||||
"""Attach lazy-load behavior when nodes are expanded."""
|
|
||||||
self.tree.bind("<<TreeviewOpen>>", self._on_open_node)
|
|
||||||
|
|
||||||
def refresh_all(self):
|
|
||||||
"""Reload both the duplication tree and the summary percentage table."""
|
|
||||||
self._load_corsie()
|
|
||||||
self._load_riepilogo()
|
|
||||||
|
|
||||||
def _load_corsie(self):
|
|
||||||
"""Load root nodes representing aisles with duplicated cells."""
|
|
||||||
self.tree.delete(*self.tree.get_children())
|
|
||||||
|
|
||||||
async def _q(db):
|
|
||||||
return await db.query_json(SQL_CORSIE, as_dict_rows=True)
|
|
||||||
|
|
||||||
self.runner.run(_q(self.db), self._fill_corsie, lambda e: messagebox.showerror("Errore", str(e), parent=self))
|
|
||||||
|
|
||||||
def _fill_corsie(self, res):
|
|
||||||
"""Populate root tree nodes after the aisle query completes."""
|
|
||||||
rows = _json_obj(res).get("rows", [])
|
|
||||||
for row in rows:
|
|
||||||
corsia = row.get("Corsia")
|
|
||||||
if not corsia:
|
|
||||||
continue
|
|
||||||
node_id = f"corsia:{corsia}"
|
|
||||||
self.tree.insert("", "end", iid=node_id, text=f"Corsia {corsia}", values=("", ""), open=False, tags=("corsia",))
|
|
||||||
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", ""))
|
|
||||||
|
|
||||||
def _on_open_node(self, _evt):
|
|
||||||
"""Lazy-load children when a tree node is expanded."""
|
|
||||||
sel = self.tree.focus()
|
|
||||||
if not sel:
|
|
||||||
return
|
|
||||||
if sel.startswith("corsia:"):
|
|
||||||
lazy_id = f"{sel}::lazy"
|
|
||||||
if lazy_id in self.tree.get_children(sel):
|
|
||||||
self.tree.delete(lazy_id)
|
|
||||||
corsia = sel.split(":", 1)[1]
|
|
||||||
self._load_celle_for_corsia(sel, corsia)
|
|
||||||
elif sel.startswith("cella:"):
|
|
||||||
lazy_id = f"{sel}::lazy"
|
|
||||||
if lazy_id in self.tree.get_children(sel):
|
|
||||||
self.tree.delete(lazy_id)
|
|
||||||
idcella = int(sel.split(":", 1)[1])
|
|
||||||
for child in self.tree.get_children(sel):
|
|
||||||
self.tree.delete(child)
|
|
||||||
self._load_pallet_for_cella(sel, idcella)
|
|
||||||
|
|
||||||
def _load_celle_for_corsia(self, parent_iid, corsia):
|
|
||||||
"""Query duplicated cells for the selected aisle."""
|
|
||||||
async def _q(db):
|
|
||||||
return await db.query_json(SQL_CELLE_DUP_PER_CORSIA, params={"corsia": corsia}, as_dict_rows=True)
|
|
||||||
|
|
||||||
self.runner.run(_q(self.db), lambda res: self._fill_celle(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self))
|
|
||||||
|
|
||||||
def _fill_celle(self, parent_iid, res):
|
|
||||||
"""Populate duplicated-cell nodes under an aisle node."""
|
|
||||||
rows = _json_obj(res).get("rows", [])
|
|
||||||
if not rows:
|
|
||||||
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", ""))
|
|
||||||
return
|
|
||||||
for row in rows:
|
|
||||||
idc = row["IDCella"]
|
|
||||||
ubi = row["Ubicazione"]
|
|
||||||
corsia = row.get("Corsia")
|
|
||||||
num = row.get("NumUDC", 0)
|
|
||||||
node_id = f"cella:{idc}"
|
|
||||||
label = f"{ubi} [x{num}]"
|
|
||||||
if self.tree.exists(node_id):
|
|
||||||
self.tree.item(node_id, text=label, values=(f"IDCella {idc}", ""))
|
|
||||||
else:
|
|
||||||
self.tree.insert(parent_iid, "end", iid=node_id, text=label, values=(f"IDCella {idc}", ""), open=False, tags=("cella", f"corsia:{corsia}"))
|
|
||||||
if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)):
|
|
||||||
self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", ""))
|
|
||||||
|
|
||||||
def _load_pallet_for_cella(self, parent_iid, idcella: int):
|
|
||||||
"""Query pallet details for a duplicated cell."""
|
|
||||||
async def _q(db):
|
|
||||||
return await db.query_json(SQL_PALLET_IN_CELLA, params={"idcella": idcella}, as_dict_rows=True)
|
|
||||||
|
|
||||||
self.runner.run(_q(self.db), lambda res: self._fill_pallet(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self))
|
|
||||||
|
|
||||||
def _fill_pallet(self, parent_iid, res):
|
|
||||||
"""Add pallet leaves under the selected cell node."""
|
|
||||||
rows = _json_obj(res).get("rows", [])
|
|
||||||
if not rows:
|
|
||||||
self.tree.insert(parent_iid, "end", text="(nessun pallet)", values=("", ""))
|
|
||||||
return
|
|
||||||
parent_tags = self.tree.item(parent_iid, "tags") or ()
|
|
||||||
corsia_tag = next((tag for tag in parent_tags if tag.startswith("corsia:")), None)
|
|
||||||
corsia_val = corsia_tag.split(":", 1)[1] if corsia_tag else ""
|
|
||||||
cella_ubi = self.tree.item(parent_iid, "text")
|
|
||||||
idcella_txt = self.tree.item(parent_iid, "values")[0]
|
|
||||||
idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
pallet = row.get("Pallet", "")
|
|
||||||
desc = row.get("Descrizione", "")
|
|
||||||
lotto = row.get("Lotto", "")
|
|
||||||
leaf_id = f"pallet:{idcella_num}:{pallet}"
|
|
||||||
if self.tree.exists(leaf_id):
|
|
||||||
self.tree.item(leaf_id, text=str(pallet), values=(desc, lotto))
|
|
||||||
continue
|
|
||||||
self.tree.insert(
|
|
||||||
parent_iid,
|
|
||||||
"end",
|
|
||||||
iid=leaf_id,
|
|
||||||
text=str(pallet),
|
|
||||||
values=(desc, lotto),
|
|
||||||
tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _load_riepilogo(self):
|
|
||||||
"""Load the percentage summary by aisle."""
|
|
||||||
async def _q(db):
|
|
||||||
return await db.query_json(SQL_RIEPILOGO_PERCENTUALI, as_dict_rows=True)
|
|
||||||
|
|
||||||
self.runner.run(_q(self.db), self._fill_riepilogo, lambda e: messagebox.showerror("Errore", str(e), parent=self))
|
|
||||||
|
|
||||||
def _fill_riepilogo(self, res):
|
|
||||||
"""Refresh the bottom summary table."""
|
|
||||||
rows = _json_obj(res).get("rows", [])
|
|
||||||
for item in self.sum_tbl.get_children():
|
|
||||||
self.sum_tbl.delete(item)
|
|
||||||
for row in rows:
|
|
||||||
self.sum_tbl.insert(
|
|
||||||
"",
|
|
||||||
"end",
|
|
||||||
values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def expand_all(self):
|
|
||||||
"""Expand all aisle roots and trigger lazy loading where needed."""
|
|
||||||
for iid in self.tree.get_children(""):
|
|
||||||
self.tree.item(iid, open=True)
|
|
||||||
if f"{iid}::lazy" in self.tree.get_children(iid):
|
|
||||||
self.tree.delete(f"{iid}::lazy")
|
|
||||||
corsia = iid.split(":", 1)[1]
|
|
||||||
self._load_celle_for_corsia(iid, corsia)
|
|
||||||
|
|
||||||
def collapse_all(self):
|
|
||||||
"""Collapse all root nodes in the duplication tree."""
|
|
||||||
for iid in self.tree.get_children(""):
|
|
||||||
self.tree.item(iid, open=False)
|
|
||||||
|
|
||||||
def export_to_xlsx(self):
|
|
||||||
"""Export both the detailed tree and the summary table to Excel."""
|
|
||||||
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
|
|
||||||
default_name = f"esportazione_celle_udc_multiple_{ts}.xlsx"
|
|
||||||
fname = filedialog.asksaveasfilename(
|
|
||||||
parent=self,
|
|
||||||
title="Esporta in Excel",
|
|
||||||
defaultextension=".xlsx",
|
|
||||||
filetypes=[("Excel Workbook", "*.xlsx")],
|
|
||||||
initialfile=default_name,
|
|
||||||
)
|
|
||||||
if not fname:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
wb = Workbook()
|
|
||||||
ws_det = wb.active
|
|
||||||
ws_det.title = "Dettaglio"
|
|
||||||
ws_sum = wb.create_sheet("Riepilogo")
|
|
||||||
det_headers = ["Corsia", "Ubicazione", "IDCella", "Pallet", "Descrizione", "Lotto"]
|
|
||||||
sum_headers = ["Corsia", "TotCelle", "CelleMultiple", "Percentuale"]
|
|
||||||
|
|
||||||
def _hdr(ws, headers):
|
|
||||||
"""Write formatted headers into the given worksheet."""
|
|
||||||
for j, header in enumerate(headers, start=1):
|
|
||||||
cell = ws.cell(row=1, column=j, value=header)
|
|
||||||
cell.font = Font(bold=True)
|
|
||||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
||||||
|
|
||||||
_hdr(ws_det, det_headers)
|
|
||||||
_hdr(ws_sum, sum_headers)
|
|
||||||
|
|
||||||
row_idx = 2
|
|
||||||
for corsia_node in self.tree.get_children(""):
|
|
||||||
for cella_node in self.tree.get_children(corsia_node):
|
|
||||||
for pallet_node in self.tree.get_children(cella_node):
|
|
||||||
tags = self.tree.item(pallet_node, "tags") or ()
|
|
||||||
if "pallet" not in tags:
|
|
||||||
continue
|
|
||||||
corsia = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("corsia:")), "")
|
|
||||||
ubi = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("ubicazione:")), "")
|
|
||||||
idcella = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("idcella:")), "")
|
|
||||||
pallet = self.tree.item(pallet_node, "text")
|
|
||||||
desc, lotto = self.tree.item(pallet_node, "values")
|
|
||||||
for j, value in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1):
|
|
||||||
ws_det.cell(row=row_idx, column=j, value=value)
|
|
||||||
row_idx += 1
|
|
||||||
|
|
||||||
row_idx = 2
|
|
||||||
for iid in self.sum_tbl.get_children(""):
|
|
||||||
vals = self.sum_tbl.item(iid, "values")
|
|
||||||
for j, value in enumerate(vals, start=1):
|
|
||||||
ws_sum.cell(row=row_idx, column=j, value=value)
|
|
||||||
row_idx += 1
|
|
||||||
|
|
||||||
def _autosize(ws):
|
|
||||||
"""Resize worksheet columns based on their longest value."""
|
|
||||||
widths = {}
|
|
||||||
for row in ws.iter_rows(values_only=True):
|
|
||||||
for j, value in enumerate(row, start=1):
|
|
||||||
value_s = "" if value is None else str(value)
|
|
||||||
widths[j] = max(widths.get(j, 0), len(value_s))
|
|
||||||
from openpyxl.utils import get_column_letter
|
|
||||||
|
|
||||||
for j, width in widths.items():
|
|
||||||
ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60)
|
|
||||||
|
|
||||||
_autosize(ws_det)
|
|
||||||
_autosize(ws_sum)
|
|
||||||
wb.save(fname)
|
|
||||||
messagebox.showinfo("Esportazione completata", f"File creato:\n{fname}", parent=self)
|
|
||||||
except Exception as ex:
|
|
||||||
messagebox.showerror("Errore esportazione", str(ex), parent=self)
|
|
||||||
|
|
||||||
|
|
||||||
def open_celle_multiple_window(root: tk.Tk, db_client, runner: AsyncRunner | None = None):
|
|
||||||
"""Create, focus and return the duplicated-cells explorer."""
|
|
||||||
win = CelleMultipleWindow(root, db_client, runner=runner)
|
|
||||||
win.lift()
|
|
||||||
win.focus_set()
|
|
||||||
return win
|
|
||||||
8
warehouse.pyw
Normal file
8
warehouse.pyw
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from runtime_support import ensure_stdio, run_with_fatal_log
|
||||||
|
|
||||||
|
ensure_stdio("warehouse_main")
|
||||||
|
|
||||||
|
from main import run_app
|
||||||
|
|
||||||
|
|
||||||
|
raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))
|
||||||
592
window_placement.py
Normal file
592
window_placement.py
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
"""Helpers to place child windows consistently relative to the launcher."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import tkinter as tk
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_LOG_NAME = "window_placement"
|
||||||
|
MODULE_LOG_PATH = Path(__file__).with_name("window_placement.log")
|
||||||
|
_MODULE_LOGGER = logging.getLogger(MODULE_LOG_NAME)
|
||||||
|
if not _MODULE_LOGGER.handlers:
|
||||||
|
_MODULE_LOGGER.setLevel(logging.DEBUG)
|
||||||
|
_MODULE_LOGGER.propagate = False
|
||||||
|
_handler = logging.FileHandler(MODULE_LOG_PATH, encoding="utf-8")
|
||||||
|
_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
|
||||||
|
_MODULE_LOGGER.addHandler(_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_xy(window: tk.Misc) -> tuple[int | None, int | None]:
|
||||||
|
"""Return current window coordinates without raising."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(window.winfo_x()), int(window.winfo_y())
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_wh(window: tk.Misc) -> tuple[int | None, int | None]:
|
||||||
|
"""Return current window size without raising."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(window.winfo_width()), int(window.winfo_height())
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _window_label(window: tk.Misc) -> str:
|
||||||
|
"""Return a readable label for log messages."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = str(window.title()).strip()
|
||||||
|
if title:
|
||||||
|
return title
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return str(window)
|
||||||
|
except Exception:
|
||||||
|
return "<window>"
|
||||||
|
|
||||||
|
|
||||||
|
def _work_area_bounds(window: tk.Misc) -> tuple[int, int, int, int]:
|
||||||
|
"""Return the desktop work area excluding the taskbar when available."""
|
||||||
|
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
|
rect = RECT()
|
||||||
|
SPI_GETWORKAREA = 0x0030
|
||||||
|
if ctypes.windll.user32.SystemParametersInfoW(SPI_GETWORKAREA, 0, ctypes.byref(rect), 0):
|
||||||
|
return int(rect.left), int(rect.top), int(rect.right), int(rect.bottom)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
before_x, before_y = _safe_xy(child)
|
||||||
|
before_w, before_h = _safe_wh(child)
|
||||||
|
_MODULE_LOGGER.debug(
|
||||||
|
"set_bounds.start window=%s from=(%s,%s,%s,%s) target=(%s,%s,%s,%s)",
|
||||||
|
_window_label(child),
|
||||||
|
before_x,
|
||||||
|
before_y,
|
||||||
|
before_w,
|
||||||
|
before_h,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
child.state("normal")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
child.deiconify()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(ctypes, "windll"):
|
||||||
|
hwnd = int(child.winfo_id())
|
||||||
|
SWP_NOZORDER = 0x0004
|
||||||
|
SWP_NOACTIVATE = 0x0010
|
||||||
|
flags = SWP_NOZORDER | SWP_NOACTIVATE
|
||||||
|
move_w = 0 if width is None else int(width)
|
||||||
|
move_h = 0 if height is None else int(height)
|
||||||
|
if width is None or height is None:
|
||||||
|
flags |= 0x0001 # SWP_NOSIZE
|
||||||
|
ctypes.windll.user32.SetWindowPos(hwnd, 0, int(x), int(y), move_w, move_h, flags)
|
||||||
|
if width is None or height is None:
|
||||||
|
child.geometry(f"+{x}+{y}")
|
||||||
|
else:
|
||||||
|
child.geometry(f"{int(width)}x{int(height)}+{x}+{y}")
|
||||||
|
except Exception:
|
||||||
|
fallback_w = max(child.winfo_width(), child.winfo_reqwidth()) if width is None else int(width)
|
||||||
|
fallback_h = max(child.winfo_height(), child.winfo_reqheight()) if height is None else int(height)
|
||||||
|
child.geometry(f"{fallback_w}x{fallback_h}+{x}+{y}")
|
||||||
|
try:
|
||||||
|
child.update_idletasks()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
after_x, after_y = _safe_xy(child)
|
||||||
|
after_w, after_h = _safe_wh(child)
|
||||||
|
_MODULE_LOGGER.debug(
|
||||||
|
"set_bounds.end window=%s final=(%s,%s,%s,%s) target=(%s,%s,%s,%s)",
|
||||||
|
_window_label(child),
|
||||||
|
after_x,
|
||||||
|
after_y,
|
||||||
|
after_w,
|
||||||
|
after_h,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_window_position(child: tk.Misc, x: int, y: int) -> None:
|
||||||
|
"""Move a toplevel to the requested screen coordinates without resizing it."""
|
||||||
|
|
||||||
|
_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."""
|
||||||
|
|
||||||
|
if not hasattr(ctypes, "windll"):
|
||||||
|
return False
|
||||||
|
if len(windows) != len(positions) or not windows:
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
SWP_NOSIZE = 0x0001
|
||||||
|
SWP_NOZORDER = 0x0004
|
||||||
|
SWP_NOACTIVATE = 0x0010
|
||||||
|
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:
|
||||||
|
child.state("normal")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
child.deiconify()
|
||||||
|
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
|
||||||
|
|
||||||
|
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``.
|
||||||
|
|
||||||
|
The placement uses root-window coordinates and preserves the child's
|
||||||
|
computed width/height. Call it after the child has a geometry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent.update_idletasks()
|
||||||
|
child.update_idletasks()
|
||||||
|
# On Windows/Tk, ``winfo_rootx`` starts at the inner client area,
|
||||||
|
# while ``winfo_x`` tracks the outer window frame. Using ``winfo_x``
|
||||||
|
# keeps child windows flush with the launcher's external left border.
|
||||||
|
x = parent.winfo_x() + int(x_offset)
|
||||||
|
y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
|
||||||
|
_set_window_position(child, x, y)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def place_window_below_parent_later(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
|
||||||
|
"""Schedule child placement on the Tk queue after geometry settles."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
child.after(0, lambda: place_window_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap))
|
||||||
|
except Exception:
|
||||||
|
place_window_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap)
|
||||||
|
|
||||||
|
|
||||||
|
def place_window_fullsize_below_parent(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
|
||||||
|
"""Place a child below the launcher and size it to the full remaining work area."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def tile_children_below_parent(parent: tk.Misc, children: list[tk.Misc], *, gap: int = 8, margin: int = 8):
|
||||||
|
"""Arrange open children in a compact grid below the launcher."""
|
||||||
|
|
||||||
|
windows = [w for w in children if w is not None and getattr(w, "winfo_exists", lambda: False)()]
|
||||||
|
if not windows:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent.update_idletasks()
|
||||||
|
start_x = parent.winfo_rootx() + int(margin)
|
||||||
|
start_y = parent.winfo_rooty() + parent.winfo_height() + int(margin)
|
||||||
|
screen_w = parent.winfo_screenwidth()
|
||||||
|
screen_h = parent.winfo_screenheight()
|
||||||
|
avail_w = max(320, screen_w - start_x - int(margin))
|
||||||
|
avail_h = max(240, screen_h - start_y - int(margin))
|
||||||
|
|
||||||
|
count = len(windows)
|
||||||
|
cols = max(1, math.ceil(math.sqrt(count)))
|
||||||
|
rows = max(1, math.ceil(count / cols))
|
||||||
|
cell_w = max(320, (avail_w - (cols - 1) * int(gap)) // cols)
|
||||||
|
cell_h = max(240, (avail_h - (rows - 1) * int(gap)) // rows)
|
||||||
|
|
||||||
|
for idx, child in enumerate(windows):
|
||||||
|
row = idx // cols
|
||||||
|
col = idx % cols
|
||||||
|
x = start_x + col * (cell_w + int(gap))
|
||||||
|
y = start_y + row * (cell_h + int(gap))
|
||||||
|
child.geometry(f"{cell_w}x{cell_h}+{x}+{y}")
|
||||||
|
try:
|
||||||
|
child.lift()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cascade_children_below_parent(
|
||||||
|
parent: tk.Misc,
|
||||||
|
children: list[tk.Misc],
|
||||||
|
*,
|
||||||
|
x_offset_step: int = 20,
|
||||||
|
y_offset_step: int = 20,
|
||||||
|
margin_left: int = 0,
|
||||||
|
margin_top: int = 0,
|
||||||
|
):
|
||||||
|
"""Arrange open children in cascade order below the launcher."""
|
||||||
|
|
||||||
|
windows = [w for w in children if w is not None and getattr(w, "winfo_exists", lambda: False)()]
|
||||||
|
if not windows:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent.update_idletasks()
|
||||||
|
base_x = parent.winfo_x() + int(margin_left)
|
||||||
|
base_y = parent.winfo_rooty() + parent.winfo_height() + int(margin_top)
|
||||||
|
positions: list[tuple[int, int]] = []
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
"cascade.start parent=%s base=(%s,%s) count=%s x_step=%s y_step=%s",
|
||||||
|
_window_label(parent),
|
||||||
|
base_x,
|
||||||
|
base_y,
|
||||||
|
len(windows),
|
||||||
|
x_offset_step,
|
||||||
|
y_offset_step,
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, child in enumerate(windows):
|
||||||
|
child.update_idletasks()
|
||||||
|
x = base_x + idx * int(x_offset_step)
|
||||||
|
y = base_y + idx * int(y_offset_step)
|
||||||
|
positions.append((x, y))
|
||||||
|
_MODULE_LOGGER.info(
|
||||||
|
"cascade.window index=%s window=%s target=(%s,%s)",
|
||||||
|
idx,
|
||||||
|
_window_label(child),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
)
|
||||||
|
|
||||||
|
batched = _batch_set_window_positions(windows, positions)
|
||||||
|
_MODULE_LOGGER.info("cascade.batch_applied=%s", batched)
|
||||||
|
|
||||||
|
for child, (x, y) in zip(windows, positions):
|
||||||
|
try:
|
||||||
|
if not batched:
|
||||||
|
_set_window_position(child, x, y)
|
||||||
|
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.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:
|
||||||
|
_MODULE_LOGGER.exception("cascade.error")
|
||||||
Reference in New Issue
Block a user