chore: initial commit
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
10
.idea/misc.xml
generated
Normal file
10
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.13" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/warehouse.iml" filepath="$PROJECT_DIR$/.idea/warehouse.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/warehouse.iml
generated
Normal file
12
.idea/warehouse.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
BIN
__pycache__/async_loop_singleton.cpython-313.pyc
Normal file
BIN
__pycache__/async_loop_singleton.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/async_msssql_query.cpython-313.pyc
Normal file
BIN
__pycache__/async_msssql_query.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/async_runner.cpython-313.pyc
Normal file
BIN
__pycache__/async_runner.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/db_async_singleton.cpython-313.pyc
Normal file
BIN
__pycache__/db_async_singleton.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/gestione_aree_frame_async.cpython-313.pyc
Normal file
BIN
__pycache__/gestione_aree_frame_async.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/gestione_pickinglist.cpython-313.pyc
Normal file
BIN
__pycache__/gestione_pickinglist.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/layout_window.cpython-313.pyc
Normal file
BIN
__pycache__/layout_window.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/prenota_sprenota_sql.cpython-313.pyc
Normal file
BIN
__pycache__/prenota_sprenota_sql.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/reset_corsie.cpython-313.pyc
Normal file
BIN
__pycache__/reset_corsie.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/search_pallets.cpython-313.pyc
Normal file
BIN
__pycache__/search_pallets.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/view_celle_multiple.cpython-313.pyc
Normal file
BIN
__pycache__/view_celle_multiple.cpython-313.pyc
Normal file
Binary file not shown.
BIN
assets/fonts/Font Awesome 7 Brands-Regular-400.otf
Normal file
BIN
assets/fonts/Font Awesome 7 Brands-Regular-400.otf
Normal file
Binary file not shown.
BIN
assets/fonts/Font Awesome 7 Free-Regular-400.otf
Normal file
BIN
assets/fonts/Font Awesome 7 Free-Regular-400.otf
Normal file
Binary file not shown.
BIN
assets/fonts/Font Awesome 7 Free-Solid-900.otf
Normal file
BIN
assets/fonts/Font Awesome 7 Free-Solid-900.otf
Normal file
Binary file not shown.
BIN
assets/icons/home.png
Normal file
BIN
assets/icons/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
35
async_loop_singleton.py
Normal file
35
async_loop_singleton.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# async_loop_singleton.py
|
||||||
|
import asyncio, threading
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
class _LoopHolder:
|
||||||
|
def __init__(self):
|
||||||
|
self.loop = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
_GLOBAL = _LoopHolder()
|
||||||
|
|
||||||
|
def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||||
|
"""Start a single asyncio loop in a background thread and return it."""
|
||||||
|
if _GLOBAL.loop:
|
||||||
|
return _GLOBAL.loop
|
||||||
|
|
||||||
|
ready = threading.Event()
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
_GLOBAL.loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(_GLOBAL.loop)
|
||||||
|
ready.set()
|
||||||
|
_GLOBAL.loop.run_forever()
|
||||||
|
|
||||||
|
_GLOBAL.thread = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
|
||||||
|
_GLOBAL.thread.start()
|
||||||
|
ready.wait()
|
||||||
|
return _GLOBAL.loop
|
||||||
|
|
||||||
|
def stop_global_loop():
|
||||||
|
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
||||||
|
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
||||||
|
_GLOBAL.thread.join(timeout=2)
|
||||||
|
_GLOBAL.loop = None
|
||||||
|
_GLOBAL.thread = None
|
||||||
93
async_msssql_query.py
Normal file
93
async_msssql_query.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# async_msssql_query.py — loop-safe, compat rows=list, no pooling
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio, urllib.parse, time, logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy.pool import NullPool
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
try:
|
||||||
|
import orjson as _json
|
||||||
|
def _dumps(obj: Any) -> str: return _json.dumps(obj, default=str).decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
import json as _json
|
||||||
|
def _dumps(obj: Any) -> str: return _json.dumps(obj, default=str)
|
||||||
|
|
||||||
|
def make_mssql_dsn(
|
||||||
|
*, server: str, database: str, user: Optional[str]=None, password: Optional[str]=None,
|
||||||
|
driver: str="ODBC Driver 17 for SQL Server", trust_server_certificate: bool=True,
|
||||||
|
encrypt: Optional[str]=None, extra_odbc_kv: Optional[Dict[str,str]]=None
|
||||||
|
) -> str:
|
||||||
|
kv = {"DRIVER": driver, "SERVER": server, "DATABASE": database,
|
||||||
|
"TrustServerCertificate": "Yes" if trust_server_certificate else "No"}
|
||||||
|
if user: kv["UID"] = user
|
||||||
|
if password: kv["PWD"] = password
|
||||||
|
if encrypt: kv["Encrypt"] = encrypt
|
||||||
|
if extra_odbc_kv: kv.update(extra_odbc_kv)
|
||||||
|
odbc = ";".join(f"{k}={v}" for k,v in kv.items()) + ";"
|
||||||
|
return f"mssql+aioodbc:///?odbc_connect={urllib.parse.quote_plus(odbc)}"
|
||||||
|
|
||||||
|
class AsyncMSSQLClient:
|
||||||
|
"""
|
||||||
|
Engine creato pigramente sul loop corrente, senza pool (NullPool).
|
||||||
|
Evita “Future attached to a different loop” nei reset/close del pool.
|
||||||
|
"""
|
||||||
|
def __init__(self, dsn: str, *, echo: bool=False, log: bool=True):
|
||||||
|
self._dsn = dsn
|
||||||
|
self._echo = echo
|
||||||
|
self._engine = None
|
||||||
|
self._engine_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
self._logger = logging.getLogger("AsyncMSSQLClient")
|
||||||
|
if log and not self._logger.handlers:
|
||||||
|
h = logging.StreamHandler()
|
||||||
|
h.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
||||||
|
self._logger.addHandler(h)
|
||||||
|
self._enable_log = log
|
||||||
|
|
||||||
|
async def _ensure_engine(self):
|
||||||
|
if self._engine is not None:
|
||||||
|
return
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
self._engine = create_async_engine(
|
||||||
|
self._dsn,
|
||||||
|
echo=self._echo,
|
||||||
|
# IMPORTANTI:
|
||||||
|
poolclass=NullPool, # no pooling → no reset su loop “sbagliati”
|
||||||
|
connect_args={"loop": loop}, # usa il loop corrente in aioodbc
|
||||||
|
)
|
||||||
|
self._engine_loop = loop
|
||||||
|
if self._enable_log:
|
||||||
|
self._logger.info("Engine created on loop %s", id(loop))
|
||||||
|
|
||||||
|
async def dispose(self):
|
||||||
|
if self._engine is None:
|
||||||
|
return
|
||||||
|
# sempre sullo stesso loop in cui è nato
|
||||||
|
if asyncio.get_running_loop() is self._engine_loop:
|
||||||
|
await self._engine.dispose()
|
||||||
|
else:
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(self._engine.dispose(), self._engine_loop)
|
||||||
|
fut.result(timeout=2)
|
||||||
|
self._engine = None
|
||||||
|
if self._enable_log:
|
||||||
|
self._logger.info("Engine disposed")
|
||||||
|
|
||||||
|
async def query_json(self, sql: str, params: Optional[Dict[str, Any]]=None, *, as_dict_rows: bool=False) -> Dict[str, Any]:
|
||||||
|
await self._ensure_engine()
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
async with self._engine.connect() as conn:
|
||||||
|
res = await conn.execute(text(sql), params or {})
|
||||||
|
rows = res.fetchall()
|
||||||
|
cols = list(res.keys())
|
||||||
|
if as_dict_rows:
|
||||||
|
rows_out = [dict(zip(cols, r)) for r in rows]
|
||||||
|
else:
|
||||||
|
rows_out = [list(r) for r in rows]
|
||||||
|
return {"columns": cols, "rows": rows_out, "elapsed_ms": round((time.perf_counter()-t0)*1000, 3)}
|
||||||
|
|
||||||
|
async def exec(self, sql: str, params: Optional[Dict[str, Any]]=None, *, commit: bool=False) -> int:
|
||||||
|
await self._ensure_engine()
|
||||||
|
async with (self._engine.begin() if commit else self._engine.connect()) as conn:
|
||||||
|
res = await conn.execute(text(sql), params or {})
|
||||||
|
return res.rowcount or 0
|
||||||
25
async_runner.py
Normal file
25
async_runner.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# async_runner.py
|
||||||
|
import asyncio
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
class AsyncRunner:
|
||||||
|
"""Esegue un awaitable sul loop globale e richiama i callback in Tk via .after."""
|
||||||
|
def __init__(self, tk_root, loop: asyncio.AbstractEventLoop):
|
||||||
|
self.tk = tk_root
|
||||||
|
self.loop = loop
|
||||||
|
|
||||||
|
def run(self, awaitable, on_ok: Callable, on_err: Callable, busy=None, message: str | None=None):
|
||||||
|
if busy: busy.show(message or "Lavoro in corso…")
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop)
|
||||||
|
self._poll(fut, on_ok, on_err, busy)
|
||||||
|
|
||||||
|
def _poll(self, fut, on_ok, on_err, busy):
|
||||||
|
if fut.done():
|
||||||
|
if busy: busy.hide()
|
||||||
|
try:
|
||||||
|
res = fut.result()
|
||||||
|
on_ok(res)
|
||||||
|
except Exception as ex:
|
||||||
|
on_err(ex)
|
||||||
|
return
|
||||||
|
self.tk.after(50, lambda: self._poll(fut, on_ok, on_err, busy))
|
||||||
148
backups/autosave.tcl.bak1
Normal file
148
backups/autosave.tcl.bak1
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#############################################################################
|
||||||
|
# Generated by PAGE version 8.0
|
||||||
|
# in conjunction with Tcl version 8.6
|
||||||
|
# Sep 15, 2025 05:35:31 PM CEST platform: Windows NT
|
||||||
|
set vTcl(timestamp) ""
|
||||||
|
if {![info exists vTcl(borrow)]} {
|
||||||
|
::vTcl::MessageBox -title Error -message "You must open project files from within PAGE."
|
||||||
|
exit}
|
||||||
|
|
||||||
|
|
||||||
|
set vTcl(actual_gui_font_dft_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_dft_name) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_text_desc) TkTextFont
|
||||||
|
set vTcl(actual_gui_font_text_name) TkTextFont
|
||||||
|
set vTcl(actual_gui_font_fixed_desc) TkFixedFont
|
||||||
|
set vTcl(actual_gui_font_fixed_name) TkFixedFont
|
||||||
|
set vTcl(actual_gui_font_menu_desc) TkMenuFont
|
||||||
|
set vTcl(actual_gui_font_menu_name) TkMenuFont
|
||||||
|
set vTcl(actual_gui_font_tooltip_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_tooltip_name) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_treeview_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_treeview_name) TkDefaultFont
|
||||||
|
###########################################
|
||||||
|
set vTcl(actual_gui_bg) #d9d9d9
|
||||||
|
set vTcl(actual_gui_fg) #000000
|
||||||
|
set vTcl(actual_gui_analog) #ececec
|
||||||
|
set vTcl(actual_gui_menu_analog) #ececec
|
||||||
|
set vTcl(actual_gui_menu_bg) #d9d9d9
|
||||||
|
set vTcl(actual_gui_menu_fg) #000000
|
||||||
|
set vTcl(complement_color) gray40
|
||||||
|
set vTcl(analog_color_p) #c3c3c3
|
||||||
|
set vTcl(analog_color_m) beige
|
||||||
|
set vTcl(tabfg1) black
|
||||||
|
set vTcl(tabfg2) white
|
||||||
|
set vTcl(actual_gui_menu_active_bg) #ececec
|
||||||
|
set vTcl(actual_gui_menu_active_fg) #000000
|
||||||
|
###########################################
|
||||||
|
set vTcl(pr,autoalias) 1
|
||||||
|
set vTcl(pr,relative_placement) 1
|
||||||
|
set vTcl(mode) Relative
|
||||||
|
set vTcl(project_theme) default
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc vTclWindow.top1 {base} {
|
||||||
|
global vTcl
|
||||||
|
if {$base == ""} {
|
||||||
|
set base .top1
|
||||||
|
}
|
||||||
|
if {[winfo exists $base]} {
|
||||||
|
wm deiconify $base; return
|
||||||
|
}
|
||||||
|
set top $base
|
||||||
|
set target $base
|
||||||
|
###################
|
||||||
|
# CREATING WIDGETS
|
||||||
|
###################
|
||||||
|
vTcl::widgets::core::toplevel::createCmd $top -class Toplevel \
|
||||||
|
-background #d9d9d9 -highlightbackground #d9d9d9 \
|
||||||
|
-highlightcolor #000000
|
||||||
|
wm focusmodel $top passive
|
||||||
|
wm geometry $top 600x450+374+144
|
||||||
|
update
|
||||||
|
# set in toplevel.wgt.
|
||||||
|
global vTcl
|
||||||
|
global img_list
|
||||||
|
set vTcl(save,dflt,origin) 0
|
||||||
|
wm maxsize $top 3364 881
|
||||||
|
wm minsize $top 120 1
|
||||||
|
wm overrideredirect $top 0
|
||||||
|
wm resizable $top 1 1
|
||||||
|
wm deiconify $top
|
||||||
|
set toptitle "Toplevel 0"
|
||||||
|
wm title $top $toptitle
|
||||||
|
namespace eval ::widgets::${top}::ClassOption {}
|
||||||
|
set ::widgets::${top}::ClassOption(-toptitle) $toptitle
|
||||||
|
vTcl:DefineAlias "$top" "Toplevel1" vTcl:Toplevel:WidgetProc "" 1
|
||||||
|
set vTcl(real_top) {}
|
||||||
|
button "$top.but47" \
|
||||||
|
-activebackground #d9d9d9 -activeforeground black -background #d9d9d9 \
|
||||||
|
-disabledforeground #a3a3a3 -font "-family {Segoe UI} -size 9" \
|
||||||
|
-foreground #000000 -highlightbackground #d9d9d9 \
|
||||||
|
-highlightcolor #000000 -text "Button"
|
||||||
|
vTcl:DefineAlias "$top.but47" "Button1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
frame "$top.fra48" \
|
||||||
|
-borderwidth 2 -relief groove -background #d9d9d9 -height 355 \
|
||||||
|
-highlightbackground #d9d9d9 -highlightcolor #000000 -width 565
|
||||||
|
vTcl:DefineAlias "$top.fra48" "Frame1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
set site_3_0 $top.fra48
|
||||||
|
vTcl::widgets::ttk::pnotebook::createCmd "$site_3_0.pNo49" \
|
||||||
|
-width 512 -height 304 -style "PC.TNotebook" -style PC.TNotebook
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49" "PNotebook1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 configure -style "PC.TNotebook"
|
||||||
|
bind $site_3_0.pNo49 <Button-1> {
|
||||||
|
_button_press
|
||||||
|
}
|
||||||
|
bind $site_3_0.pNo49 <ButtonRelease-1> {
|
||||||
|
_button_release
|
||||||
|
}
|
||||||
|
bind $site_3_0.pNo49 <Motion> {
|
||||||
|
_mouse_over
|
||||||
|
}
|
||||||
|
ttk::frame "$site_3_0.pNo49.t0"
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49.t0" "PNotebook1_t1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 add $site_3_0.pNo49.t0 \
|
||||||
|
-padding 0 -sticky nsew -state normal -text "Page 1" -image image6 \
|
||||||
|
-compound right -underline -1
|
||||||
|
set site_5_0 $site_3_0.pNo49.t0
|
||||||
|
ttk::frame "$site_3_0.pNo49.t1"
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49.t1" "PNotebook1_t2" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 add $site_3_0.pNo49.t1 \
|
||||||
|
-padding 0 -sticky nsew -state normal -text "Page 2" -image image6 \
|
||||||
|
-compound right -underline -1
|
||||||
|
set site_5_1 $site_3_0.pNo49.t1
|
||||||
|
place $site_3_0.pNo49 \
|
||||||
|
-in $site_3_0 -x 0 -relx 0.035 -y 0 -rely 0.056 -width 0 \
|
||||||
|
-relwidth 0.906 -height 0 -relheight 0.856 -anchor nw \
|
||||||
|
-bordermode ignore
|
||||||
|
###################
|
||||||
|
# SETTING GEOMETRY
|
||||||
|
###################
|
||||||
|
place $top.but47 \
|
||||||
|
-in $top -x 0 -relx 0.033 -y 0 -rely 0.022 -width 97 -relwidth 0 \
|
||||||
|
-height 46 -relheight 0 -anchor nw -bordermode ignore
|
||||||
|
place $top.fra48 \
|
||||||
|
-in $top -x 0 -relx 0.033 -y 0 -rely 0.178 -width 0 -relwidth 0.942 \
|
||||||
|
-height 0 -relheight 0.789 -anchor nw -bordermode ignore
|
||||||
|
|
||||||
|
vTcl:FireEvent $base <<Ready>>
|
||||||
|
}
|
||||||
|
|
||||||
|
proc 36 {args} {return 1}
|
||||||
|
|
||||||
|
|
||||||
|
Window show .
|
||||||
|
set btop1 ""
|
||||||
|
if {$vTcl(borrow)} {
|
||||||
|
set btop1 .bor[expr int([expr rand() * 100])]
|
||||||
|
while {[lsearch $btop1 $vTcl(tops)] != -1} {
|
||||||
|
set btop1 .bor[expr int([expr rand() * 100])]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set vTcl(btop) $btop1
|
||||||
|
Window show .top1 $btop1
|
||||||
|
if {$vTcl(borrow)} {
|
||||||
|
$btop1 configure -background plum
|
||||||
|
}
|
||||||
|
|
||||||
148
backups/autosave.tcl.bak2
Normal file
148
backups/autosave.tcl.bak2
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#############################################################################
|
||||||
|
# Generated by PAGE version 8.0
|
||||||
|
# in conjunction with Tcl version 8.6
|
||||||
|
# Sep 15, 2025 05:34:52 PM CEST platform: Windows NT
|
||||||
|
set vTcl(timestamp) ""
|
||||||
|
if {![info exists vTcl(borrow)]} {
|
||||||
|
::vTcl::MessageBox -title Error -message "You must open project files from within PAGE."
|
||||||
|
exit}
|
||||||
|
|
||||||
|
|
||||||
|
set vTcl(actual_gui_font_dft_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_dft_name) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_text_desc) TkTextFont
|
||||||
|
set vTcl(actual_gui_font_text_name) TkTextFont
|
||||||
|
set vTcl(actual_gui_font_fixed_desc) TkFixedFont
|
||||||
|
set vTcl(actual_gui_font_fixed_name) TkFixedFont
|
||||||
|
set vTcl(actual_gui_font_menu_desc) TkMenuFont
|
||||||
|
set vTcl(actual_gui_font_menu_name) TkMenuFont
|
||||||
|
set vTcl(actual_gui_font_tooltip_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_tooltip_name) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_treeview_desc) TkDefaultFont
|
||||||
|
set vTcl(actual_gui_font_treeview_name) TkDefaultFont
|
||||||
|
###########################################
|
||||||
|
set vTcl(actual_gui_bg) #d9d9d9
|
||||||
|
set vTcl(actual_gui_fg) #000000
|
||||||
|
set vTcl(actual_gui_analog) #ececec
|
||||||
|
set vTcl(actual_gui_menu_analog) #ececec
|
||||||
|
set vTcl(actual_gui_menu_bg) #d9d9d9
|
||||||
|
set vTcl(actual_gui_menu_fg) #000000
|
||||||
|
set vTcl(complement_color) gray40
|
||||||
|
set vTcl(analog_color_p) #c3c3c3
|
||||||
|
set vTcl(analog_color_m) beige
|
||||||
|
set vTcl(tabfg1) black
|
||||||
|
set vTcl(tabfg2) white
|
||||||
|
set vTcl(actual_gui_menu_active_bg) #ececec
|
||||||
|
set vTcl(actual_gui_menu_active_fg) #000000
|
||||||
|
###########################################
|
||||||
|
set vTcl(pr,autoalias) 1
|
||||||
|
set vTcl(pr,relative_placement) 1
|
||||||
|
set vTcl(mode) Relative
|
||||||
|
set vTcl(project_theme) default
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc vTclWindow.top1 {base} {
|
||||||
|
global vTcl
|
||||||
|
if {$base == ""} {
|
||||||
|
set base .top1
|
||||||
|
}
|
||||||
|
if {[winfo exists $base]} {
|
||||||
|
wm deiconify $base; return
|
||||||
|
}
|
||||||
|
set top $base
|
||||||
|
set target $base
|
||||||
|
###################
|
||||||
|
# CREATING WIDGETS
|
||||||
|
###################
|
||||||
|
vTcl::widgets::core::toplevel::createCmd $top -class Toplevel \
|
||||||
|
-background #d9d9d9 -highlightbackground #d9d9d9 \
|
||||||
|
-highlightcolor #000000
|
||||||
|
wm focusmodel $top passive
|
||||||
|
wm geometry $top 600x450+374+144
|
||||||
|
update
|
||||||
|
# set in toplevel.wgt.
|
||||||
|
global vTcl
|
||||||
|
global img_list
|
||||||
|
set vTcl(save,dflt,origin) 0
|
||||||
|
wm maxsize $top 3364 881
|
||||||
|
wm minsize $top 120 1
|
||||||
|
wm overrideredirect $top 0
|
||||||
|
wm resizable $top 1 1
|
||||||
|
wm deiconify $top
|
||||||
|
set toptitle "Toplevel 0"
|
||||||
|
wm title $top $toptitle
|
||||||
|
namespace eval ::widgets::${top}::ClassOption {}
|
||||||
|
set ::widgets::${top}::ClassOption(-toptitle) $toptitle
|
||||||
|
vTcl:DefineAlias "$top" "Toplevel1" vTcl:Toplevel:WidgetProc "" 1
|
||||||
|
set vTcl(real_top) {}
|
||||||
|
button "$top.but47" \
|
||||||
|
-activebackground #d9d9d9 -activeforeground black -background #d9d9d9 \
|
||||||
|
-disabledforeground #a3a3a3 -font "-family {Segoe UI} -size 9" \
|
||||||
|
-foreground #000000 -highlightbackground #d9d9d9 \
|
||||||
|
-highlightcolor #000000 -text "Button"
|
||||||
|
vTcl:DefineAlias "$top.but47" "Button1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
frame "$top.fra48" \
|
||||||
|
-borderwidth 2 -relief groove -background #d9d9d9 -height 355 \
|
||||||
|
-highlightbackground #d9d9d9 -highlightcolor #000000 -width 565
|
||||||
|
vTcl:DefineAlias "$top.fra48" "Frame1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
set site_3_0 $top.fra48
|
||||||
|
vTcl::widgets::ttk::pnotebook::createCmd "$site_3_0.pNo49" \
|
||||||
|
-width 300 -height 200 -style "PC.TNotebook" -style PC.TNotebook
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49" "PNotebook1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 configure -style "PC.TNotebook"
|
||||||
|
bind $site_3_0.pNo49 <Button-1> {
|
||||||
|
_button_press
|
||||||
|
}
|
||||||
|
bind $site_3_0.pNo49 <ButtonRelease-1> {
|
||||||
|
_button_release
|
||||||
|
}
|
||||||
|
bind $site_3_0.pNo49 <Motion> {
|
||||||
|
_mouse_over
|
||||||
|
}
|
||||||
|
ttk::frame "$site_3_0.pNo49.t0"
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49.t0" "PNotebook1_t1" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 add $site_3_0.pNo49.t0 \
|
||||||
|
-padding 0 -sticky nsew -state normal -text "Page 1" -image image6 \
|
||||||
|
-compound right -underline -1
|
||||||
|
set site_5_0 $site_3_0.pNo49.t0
|
||||||
|
ttk::frame "$site_3_0.pNo49.t1"
|
||||||
|
vTcl:DefineAlias "$site_3_0.pNo49.t1" "PNotebook1_t2" vTcl:WidgetProc "Toplevel1" 1
|
||||||
|
$site_3_0.pNo49 add $site_3_0.pNo49.t1 \
|
||||||
|
-padding 0 -sticky nsew -state normal -text "Page 2" -image image6 \
|
||||||
|
-compound right -underline -1
|
||||||
|
set site_5_1 $site_3_0.pNo49.t1
|
||||||
|
place $site_3_0.pNo49 \
|
||||||
|
-in $site_3_0 -x 0 -relx 0.035 -y 0 -rely 0.056 -width 0 \
|
||||||
|
-relwidth 0.535 -height 0 -relheight 0.631 -anchor nw \
|
||||||
|
-bordermode ignore
|
||||||
|
###################
|
||||||
|
# SETTING GEOMETRY
|
||||||
|
###################
|
||||||
|
place $top.but47 \
|
||||||
|
-in $top -x 0 -relx 0.033 -y 0 -rely 0.022 -width 97 -relwidth 0 \
|
||||||
|
-height 46 -relheight 0 -anchor nw -bordermode ignore
|
||||||
|
place $top.fra48 \
|
||||||
|
-in $top -x 0 -relx 0.033 -y 0 -rely 0.178 -width 0 -relwidth 0.942 \
|
||||||
|
-height 0 -relheight 0.789 -anchor nw -bordermode ignore
|
||||||
|
|
||||||
|
vTcl:FireEvent $base <<Ready>>
|
||||||
|
}
|
||||||
|
|
||||||
|
proc 36 {args} {return 1}
|
||||||
|
|
||||||
|
|
||||||
|
Window show .
|
||||||
|
set btop1 ""
|
||||||
|
if {$vTcl(borrow)} {
|
||||||
|
set btop1 .bor[expr int([expr rand() * 100])]
|
||||||
|
while {[lsearch $btop1 $vTcl(tops)] != -1} {
|
||||||
|
set btop1 .bor[expr int([expr rand() * 100])]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set vTcl(btop) $btop1
|
||||||
|
Window show .top1 $btop1
|
||||||
|
if {$vTcl(borrow)} {
|
||||||
|
$btop1 configure -background plum
|
||||||
|
}
|
||||||
|
|
||||||
4
baseline_Mediseawall.json
Normal file
4
baseline_Mediseawall.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"db": "Mediseawall",
|
||||||
|
"baseline": {}
|
||||||
|
}
|
||||||
33
db_async_singleton.py
Normal file
33
db_async_singleton.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# db_async_singleton.py
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
class AsyncDB:
|
||||||
|
def __init__(self, engine):
|
||||||
|
self.engine = engine
|
||||||
|
|
||||||
|
async def query_json(self, sql: str, params: dict):
|
||||||
|
async with self.engine.connect() as conn:
|
||||||
|
result = await conn.execute(text(sql), params)
|
||||||
|
rows = [tuple(r) for r in result]
|
||||||
|
return {"rows": rows}
|
||||||
|
|
||||||
|
_ENGINE = None
|
||||||
|
|
||||||
|
async def _make_engine_async(conn_str: str):
|
||||||
|
return create_async_engine(conn_str, pool_pre_ping=True, future=True)
|
||||||
|
|
||||||
|
def get_db(loop: asyncio.AbstractEventLoop, conn_str: str) -> AsyncDB:
|
||||||
|
"""Crea l'engine UNA volta, dentro il loop globale, e restituisce il client."""
|
||||||
|
global _ENGINE
|
||||||
|
if _ENGINE is None:
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(_make_engine_async(conn_str), loop)
|
||||||
|
_ENGINE = fut.result()
|
||||||
|
return AsyncDB(_ENGINE)
|
||||||
|
|
||||||
|
async def dispose_async():
|
||||||
|
global _ENGINE
|
||||||
|
if _ENGINE is not None:
|
||||||
|
await _ENGINE.dispose()
|
||||||
|
_ENGINE = None
|
||||||
40
fix_layout_window.py
Normal file
40
fix_layout_window.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Path default (modifica se serve)
|
||||||
|
p = Path("./layout_window.py")
|
||||||
|
if not p.exists():
|
||||||
|
raise SystemExit(f"File non trovato: {p}")
|
||||||
|
|
||||||
|
src = p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# 1) Rimuovi i parametri border_color="transparent" nelle chiamate configure(...).
|
||||||
|
# Gestiamo i casi ", border_color='transparent'" e "border_color='transparent',"
|
||||||
|
patterns = [
|
||||||
|
re.compile(r""",\s*border_color\s*=\s*["']transparent["']"""), # , border_color="transparent"
|
||||||
|
re.compile(r"""border_color\s*=\s*["']transparent["']\s*,\s*""") # border_color="transparent",
|
||||||
|
]
|
||||||
|
for pat in patterns:
|
||||||
|
src = pat.sub("", src)
|
||||||
|
|
||||||
|
# 2) Se sono rimaste virgole prima della parentesi di chiusura: ", )" -> ")"
|
||||||
|
src = re.sub(r",\s*\)", ")", src)
|
||||||
|
|
||||||
|
# 3) (opzionale/robusto) Rimuovi border_color=None se presente in qualche versione
|
||||||
|
patterns_none = [
|
||||||
|
re.compile(r""",\s*border_color\s*=\s*None"""),
|
||||||
|
re.compile(r"""border_color\s*=\s*None\s*,\s*""")
|
||||||
|
]
|
||||||
|
for pat in patterns_none:
|
||||||
|
src = pat.sub("", src)
|
||||||
|
src = re.sub(r",\s*\)", ")", src)
|
||||||
|
|
||||||
|
# 4) NOTE: manteniamo eventuali border_color="blue" per l’highlight
|
||||||
|
|
||||||
|
# Scrivi backup e nuovo file
|
||||||
|
bak = p.with_suffix(".py.bak_fix_bc_transparent")
|
||||||
|
if not bak.exists():
|
||||||
|
bak.write_text(Path(p).read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
|
||||||
|
p.write_text(src, encoding="utf-8")
|
||||||
|
print(f"Patch applicata a {p}. Backup: {bak}")
|
||||||
87
fix_query.py
Normal file
87
fix_query.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
p = Path("./layout_window.py")
|
||||||
|
src = p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
backup = p.with_suffix(".py.bak_perf")
|
||||||
|
if not backup.exists():
|
||||||
|
backup.write_text(src, encoding="utf-8")
|
||||||
|
|
||||||
|
# 1) Rimuovi il bind su <Configure> che innescava refresh continui.
|
||||||
|
src = src.replace(
|
||||||
|
' self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))\n',
|
||||||
|
' # disabilitato: il refresh ad ogni <Configure> generava molte query/lag\n'
|
||||||
|
' # self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2) Nel metodo _refresh_stats, elimina il blocco che interroga il DB "globale".
|
||||||
|
# Riconosciamo il blocco su "sql_tot = ..." e lo neutralizziamo.
|
||||||
|
src = re.sub(
|
||||||
|
r"\n\s*# globale dal DB[\s\S]*?self\._async\.run\(.*?\)\)\n",
|
||||||
|
"\n # [patch] rimosso refresh globale da DB: calcoliamo solo dalla matrice in memoria\n",
|
||||||
|
src,
|
||||||
|
flags=re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3) Aggiungi un flag di vita finestra e un destroy sicuro
|
||||||
|
# - settiamo self._alive = True in __init__
|
||||||
|
# - override destroy() per annullare timer e marcare _alive=False
|
||||||
|
src = src.replace(
|
||||||
|
" self._last_req = 0\n",
|
||||||
|
" self._last_req = 0\n"
|
||||||
|
" self._alive = True\n"
|
||||||
|
" self._stats_after_id = None # se mai userai un refresh periodico, potremo cancellarlo qui\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# aggiungi metodo destroy() se non esiste già
|
||||||
|
if "def destroy(self):" not in src:
|
||||||
|
insert_point = src.find("def open_layout_window(")
|
||||||
|
destroy_method = (
|
||||||
|
"\n def destroy(self):\n"
|
||||||
|
" # evita nuovi refresh/async dopo destroy\n"
|
||||||
|
" self._alive = False\n"
|
||||||
|
" # cancella eventuali timer\n"
|
||||||
|
" try:\n"
|
||||||
|
" if self._stats_after_id is not None:\n"
|
||||||
|
" self.after_cancel(self._stats_after_id)\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n"
|
||||||
|
" # pulizia UI leggera\n"
|
||||||
|
" try:\n"
|
||||||
|
" for w in list(self.host.winfo_children()):\n"
|
||||||
|
" w.destroy()\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n"
|
||||||
|
" try:\n"
|
||||||
|
" super().destroy()\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n\n"
|
||||||
|
)
|
||||||
|
src = src[:insert_point] + destroy_method + src[insert_point:]
|
||||||
|
|
||||||
|
# 4) Nei callback _ok/_err delle query, assicurati che non facciano nulla se la finestra è chiusa
|
||||||
|
# => sostituiamo 'def _ok(res):' con un guard iniziale e idem per _err.
|
||||||
|
src = re.sub(
|
||||||
|
r"def _ok\(res\):\n",
|
||||||
|
"def _ok(res):\n"
|
||||||
|
" if not getattr(self, '_alive', True) or not self.winfo_exists():\n"
|
||||||
|
" return\n",
|
||||||
|
src
|
||||||
|
)
|
||||||
|
src = re.sub(
|
||||||
|
r"def _err\(ex\):\n",
|
||||||
|
"def _err(ex):\n"
|
||||||
|
" if not getattr(self, '_alive', True) or not self.winfo_exists():\n"
|
||||||
|
" return\n",
|
||||||
|
src
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5) Piccola robustezza: prima di schedulare highlight post-ricarica controlla ancora _alive
|
||||||
|
src = src.replace(
|
||||||
|
" if self._pending_focus and self._pending_focus[0] == corsia:\n",
|
||||||
|
" if getattr(self, '_alive', True) and self._pending_focus and self._pending_focus[0] == corsia:\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
p.write_text(src, encoding="utf-8")
|
||||||
|
print(f"Patch applicata a {p} (backup in {backup}).")
|
||||||
170
gestione_aree_frame_async.py
Normal file
170
gestione_aree_frame_async.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# gestione_aree_frame_async.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
import customtkinter as ctk
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
__VERSION__ = "GestioneAreeFrame v3.2.5-singleloop"
|
||||||
|
#print("[GestioneAreeFrame] loaded", __VERSION__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from async_msssql_query import AsyncMSSQLClient # noqa: F401
|
||||||
|
except Exception:
|
||||||
|
AsyncMSSQLClient = object # type: ignore
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Global asyncio loop
|
||||||
|
# ========================
|
||||||
|
class _LoopHolder:
|
||||||
|
def __init__(self):
|
||||||
|
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
self.thread: Optional[threading.Thread] = None
|
||||||
|
self.ready = threading.Event()
|
||||||
|
|
||||||
|
_GLOBAL = _LoopHolder()
|
||||||
|
|
||||||
|
def _run_loop():
|
||||||
|
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:
|
||||||
|
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():
|
||||||
|
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()
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Busy overlay
|
||||||
|
# ========================
|
||||||
|
class BusyOverlay:
|
||||||
|
def __init__(self, parent: tk.Misc):
|
||||||
|
self.parent = parent
|
||||||
|
self._top: Optional[ctk.CTkToplevel] = None
|
||||||
|
self._pb: Optional[ctk.CTkProgressBar] = None
|
||||||
|
self._lbl: Optional[ctk.CTkLabel] = None
|
||||||
|
self._bind_id = None
|
||||||
|
|
||||||
|
def _reposition(self):
|
||||||
|
if not self._top:
|
||||||
|
return
|
||||||
|
root = self.parent.winfo_toplevel()
|
||||||
|
root.update_idletasks()
|
||||||
|
x, y = root.winfo_rootx(), root.winfo_rooty()
|
||||||
|
w, h = root.winfo_width(), root.winfo_height()
|
||||||
|
self._top.geometry(f"{w}x{h}+{x}+{y}")
|
||||||
|
|
||||||
|
def show(self, message="Attendere…"):
|
||||||
|
if self._top:
|
||||||
|
if self._lbl:
|
||||||
|
self._lbl.configure(text=message)
|
||||||
|
return
|
||||||
|
root = self.parent.winfo_toplevel()
|
||||||
|
top = ctk.CTkToplevel(root)
|
||||||
|
self._top = top
|
||||||
|
top.overrideredirect(True)
|
||||||
|
try:
|
||||||
|
top.attributes("-alpha", 0.22)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
top.configure(fg_color="#000000")
|
||||||
|
except Exception:
|
||||||
|
top.configure(bg="#000000")
|
||||||
|
top.attributes("-topmost", True)
|
||||||
|
|
||||||
|
wrap = ctk.CTkFrame(top, corner_radius=8)
|
||||||
|
wrap.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
|
self._lbl = ctk.CTkLabel(wrap, text=message, font=("Segoe UI", 11, "bold"))
|
||||||
|
self._lbl.pack(pady=(0, 10))
|
||||||
|
self._pb = ctk.CTkProgressBar(wrap, mode="indeterminate", width=260)
|
||||||
|
self._pb.pack(fill="x")
|
||||||
|
try:
|
||||||
|
self._pb.start()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._reposition()
|
||||||
|
self._bind_id = root.bind("<Configure>", lambda e: self._reposition(), add="+")
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
if self._pb:
|
||||||
|
try:
|
||||||
|
self._pb.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._pb = None
|
||||||
|
if self._top:
|
||||||
|
try:
|
||||||
|
self._top.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._top = None
|
||||||
|
root = self.parent.winfo_toplevel()
|
||||||
|
if self._bind_id:
|
||||||
|
try:
|
||||||
|
root.unbind("<Configure>", self._bind_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._bind_id = None
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# AsyncRunner (single-loop)
|
||||||
|
# ========================
|
||||||
|
class AsyncRunner:
|
||||||
|
"""Run awaitables on the single global loop and callback on Tk main thread."""
|
||||||
|
def __init__(self, widget: tk.Misc):
|
||||||
|
self.widget = widget
|
||||||
|
self.loop = get_global_loop()
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
awaitable,
|
||||||
|
on_success: Callable[[Any], None],
|
||||||
|
on_error: Optional[Callable[[BaseException], None]] = None,
|
||||||
|
busy: Optional[BusyOverlay] = None,
|
||||||
|
message: str = "Operazione in corso…",
|
||||||
|
):
|
||||||
|
if busy:
|
||||||
|
busy.show(message)
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop)
|
||||||
|
|
||||||
|
def _poll():
|
||||||
|
if fut.done():
|
||||||
|
if busy:
|
||||||
|
busy.hide()
|
||||||
|
try:
|
||||||
|
res = fut.result()
|
||||||
|
except BaseException as ex:
|
||||||
|
if on_error:
|
||||||
|
self.widget.after(0, lambda e=ex: on_error(e))
|
||||||
|
else:
|
||||||
|
print("[AsyncRunner] Unhandled error:", repr(ex))
|
||||||
|
else:
|
||||||
|
self.widget.after(0, lambda r=res: on_success(r))
|
||||||
|
else:
|
||||||
|
self.widget.after(60, _poll)
|
||||||
|
|
||||||
|
_poll()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
# no-op: loop is global
|
||||||
|
pass
|
||||||
668
gestione_pickinglist.py
Normal file
668
gestione_pickinglist.py
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
# =================== gestione_pickinglist.py (NO-FLICKER + UX TUNING + MICRO-SPINNER) ===================
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import tkinter as tk
|
||||||
|
import customtkinter as ctk
|
||||||
|
from tkinter import messagebox
|
||||||
|
from typing import Optional, Any, Dict, List, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Usa overlay e runner "collaudati"
|
||||||
|
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||||
|
|
||||||
|
from async_loop_singleton import get_global_loop
|
||||||
|
from db_async_singleton import get_db as _get_db_singleton
|
||||||
|
|
||||||
|
# === IMPORT procedura async prenota/s-prenota (no pyodbc qui) ===
|
||||||
|
import asyncio
|
||||||
|
try:
|
||||||
|
from prenota_sprenota_sql import sp_xExePackingListPallet_async, SPResult
|
||||||
|
except Exception:
|
||||||
|
async def sp_xExePackingListPallet_async(*args, **kwargs):
|
||||||
|
raise RuntimeError("sp_xExePackingListPallet_async non importabile: verifica prenota_sprenota_sql.py")
|
||||||
|
class SPResult:
|
||||||
|
def __init__(self, rc=-1, message="Procedura non disponibile", id_result=None):
|
||||||
|
self.rc = rc; self.message = message; self.id_result = id_result
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- SQL --------------------
|
||||||
|
SQL_PL = """
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT Pallet) AS Pallet,
|
||||||
|
COUNT(DISTINCT Lotto) AS Lotto,
|
||||||
|
COUNT(DISTINCT Articolo) AS Articolo,
|
||||||
|
COUNT(DISTINCT Descrizione) AS Descrizione,
|
||||||
|
SUM(Qta) AS Qta,
|
||||||
|
Documento,
|
||||||
|
CodNazione,
|
||||||
|
NAZIONE,
|
||||||
|
Stato,
|
||||||
|
MAX(PalletCella) AS PalletCella,
|
||||||
|
MAX(Magazzino) AS Magazzino,
|
||||||
|
MAX(Area) AS Area,
|
||||||
|
MAX(Cella) AS Cella,
|
||||||
|
MIN(Ordinamento) AS Ordinamento,
|
||||||
|
MAX(IDStato) AS IDStato
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||||
|
ORDER BY MIN(Ordinamento), Documento, NAZIONE, Stato;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_PL_DETAILS = """
|
||||||
|
SELECT *
|
||||||
|
FROM ViewPackingListRestante
|
||||||
|
WHERE Documento = :Documento
|
||||||
|
ORDER BY Ordinamento;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -------------------- helpers --------------------
|
||||||
|
def _rows_to_dicts(res: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Converte il payload ritornato da query_json in lista di dict.
|
||||||
|
Supporta:
|
||||||
|
- res = [ {..}, {..} ]
|
||||||
|
- res = { "rows": [..], "columns": [...] }
|
||||||
|
- res = { "data": [..], "columns": [...] }
|
||||||
|
- res = { "rows": [tuple,..], "columns": [...] }
|
||||||
|
"""
|
||||||
|
if res is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(res, list):
|
||||||
|
if not res:
|
||||||
|
return []
|
||||||
|
if isinstance(res[0], dict):
|
||||||
|
return res
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(res, dict):
|
||||||
|
for rows_key in ("rows", "data", "result", "records"):
|
||||||
|
if rows_key in res and isinstance(res[rows_key], list):
|
||||||
|
rows = res[rows_key]
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
if isinstance(rows[0], dict):
|
||||||
|
return rows
|
||||||
|
cols = res.get("columns") or res.get("cols") or []
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
if cols and isinstance(r, (list, tuple)):
|
||||||
|
out.append({ (cols[i] if i < len(cols) else f"c{i}") : r[i]
|
||||||
|
for i in range(min(len(cols), len(r))) })
|
||||||
|
else:
|
||||||
|
if isinstance(r, (list, tuple)):
|
||||||
|
out.append({ f"c{i}": r[i] for i in range(len(r)) })
|
||||||
|
return out
|
||||||
|
if res and all(not isinstance(v, (list, tuple, dict)) for v in res.values()):
|
||||||
|
return [res]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _s(v) -> str:
|
||||||
|
"""Stringify safe: None -> '', altrimenti str(v)."""
|
||||||
|
return "" if v is None else str(v)
|
||||||
|
|
||||||
|
def _first(d: Dict[str, Any], keys: List[str], default: str = ""):
|
||||||
|
for k in keys:
|
||||||
|
if k in d and d[k] not in (None, ""):
|
||||||
|
return d[k]
|
||||||
|
return default
|
||||||
|
|
||||||
|
# -------------------- column specs --------------------
|
||||||
|
@dataclass
|
||||||
|
class ColSpec:
|
||||||
|
title: str
|
||||||
|
key: str
|
||||||
|
width: int
|
||||||
|
anchor: str # 'w' | 'e' | 'center'
|
||||||
|
|
||||||
|
# Colonne PL (in alto) — include IDStato per la colorazione
|
||||||
|
PL_COLS: List[ColSpec] = [
|
||||||
|
ColSpec("", "__check__", 36, "w"),
|
||||||
|
ColSpec("Documento", "Documento", 120, "w"),
|
||||||
|
ColSpec("NAZIONE", "NAZIONE", 240, "w"),
|
||||||
|
ColSpec("Stato", "Stato", 110, "w"),
|
||||||
|
ColSpec("IDStato", "IDStato", 80, "e"), # nuova colonna
|
||||||
|
ColSpec("#Pallet", "Pallet", 100, "e"),
|
||||||
|
ColSpec("#Lotti", "Lotto", 100, "e"),
|
||||||
|
ColSpec("#Articoli", "Articolo", 110, "e"),
|
||||||
|
ColSpec("Qta", "Qta", 120, "e"),
|
||||||
|
]
|
||||||
|
|
||||||
|
DET_COLS: List[ColSpec] = [
|
||||||
|
ColSpec("UDC/Pallet", "Pallet", 150, "w"),
|
||||||
|
ColSpec("Lotto", "Lotto", 130, "w"),
|
||||||
|
ColSpec("Articolo", "Articolo", 150, "w"),
|
||||||
|
ColSpec("Descrizione","Descrizione", 320, "w"),
|
||||||
|
ColSpec("Qta", "Qta", 110, "e"),
|
||||||
|
ColSpec("Ubicazione", "Ubicazione", 320, "w"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ROW_H = 28
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- Micro spinner (toolbar) --------------------
|
||||||
|
class ToolbarSpinner:
|
||||||
|
"""
|
||||||
|
Micro-animazione leggerissima per indicare attività:
|
||||||
|
mostra una label con frame: ◐ ◓ ◑ ◒ ... finché è attivo.
|
||||||
|
"""
|
||||||
|
FRAMES = ("◐", "◓", "◑", "◒")
|
||||||
|
def __init__(self, parent: tk.Widget):
|
||||||
|
self.parent = parent
|
||||||
|
self.lbl = ctk.CTkLabel(parent, text="", width=28)
|
||||||
|
self._i = 0
|
||||||
|
self._active = False
|
||||||
|
self._job = None
|
||||||
|
|
||||||
|
def widget(self) -> ctk.CTkLabel:
|
||||||
|
return self.lbl
|
||||||
|
|
||||||
|
def start(self, text: str = ""):
|
||||||
|
if self._active:
|
||||||
|
return
|
||||||
|
self._active = True
|
||||||
|
self.lbl.configure(text=f"{self.FRAMES[self._i]} {text}".strip())
|
||||||
|
self._tick()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._active = False
|
||||||
|
if self._job is not None:
|
||||||
|
try:
|
||||||
|
self.parent.after_cancel(self._job)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._job = None
|
||||||
|
self.lbl.configure(text="")
|
||||||
|
|
||||||
|
def _tick(self):
|
||||||
|
if not self._active:
|
||||||
|
return
|
||||||
|
self._i = (self._i + 1) % len(self.FRAMES)
|
||||||
|
current = self.lbl.cget("text")
|
||||||
|
# Mantieni l'eventuale testo dopo il simbolo
|
||||||
|
txt_suffix = ""
|
||||||
|
if isinstance(current, str) and len(current) > 2:
|
||||||
|
txt_suffix = current[2:]
|
||||||
|
self.lbl.configure(text=f"{self.FRAMES[self._i]}{txt_suffix}")
|
||||||
|
self._job = self.parent.after(120, self._tick) # 8 fps soft
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- Scrollable table --------------------
|
||||||
|
class ScrollTable(ctk.CTkFrame):
|
||||||
|
GRID_COLOR = "#D0D5DD"
|
||||||
|
PADX_L = 8
|
||||||
|
PADX_R = 8
|
||||||
|
PADY = 2
|
||||||
|
|
||||||
|
def __init__(self, master, columns: List[ColSpec]):
|
||||||
|
super().__init__(master)
|
||||||
|
self.columns = columns
|
||||||
|
self.total_w = sum(c.width for c in self.columns)
|
||||||
|
|
||||||
|
self.grid_rowconfigure(1, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# header
|
||||||
|
self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0)
|
||||||
|
self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color="#f3f3f3",
|
||||||
|
height=ROW_H, width=self.total_w)
|
||||||
|
self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw",
|
||||||
|
width=self.total_w, height=ROW_H)
|
||||||
|
self.h_canvas.grid(row=0, column=0, sticky="ew")
|
||||||
|
|
||||||
|
# body
|
||||||
|
self.b_canvas = tk.Canvas(self, highlightthickness=0, bd=0)
|
||||||
|
self.b_inner = ctk.CTkFrame(self.b_canvas, fg_color="transparent",
|
||||||
|
width=self.total_w)
|
||||||
|
self.body_window = self.b_canvas.create_window((0,0), window=self.b_inner,
|
||||||
|
anchor="nw", width=self.total_w)
|
||||||
|
self.b_canvas.grid(row=1, column=0, sticky="nsew")
|
||||||
|
|
||||||
|
# scrollbars
|
||||||
|
self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview)
|
||||||
|
self.xbar = tk.Scrollbar(self, orient="horizontal", command=self._xscroll_both)
|
||||||
|
self.vbar.grid(row=1, column=1, sticky="ns")
|
||||||
|
self.xbar.grid(row=2, column=0, sticky="ew")
|
||||||
|
|
||||||
|
# link scroll
|
||||||
|
self.b_canvas.configure(yscrollcommand=self.vbar.set, xscrollcommand=self._xscroll_set_both)
|
||||||
|
self.h_canvas.configure(xscrollcommand=self.xbar.set)
|
||||||
|
|
||||||
|
# bind
|
||||||
|
self.h_inner.bind("<Configure>", lambda e: self._sync_header_width())
|
||||||
|
self.b_inner.bind("<Configure>", lambda e: self._on_body_configure())
|
||||||
|
|
||||||
|
self._build_header()
|
||||||
|
|
||||||
|
def _build_header(self):
|
||||||
|
for w in self.h_inner.winfo_children():
|
||||||
|
w.destroy()
|
||||||
|
|
||||||
|
row = ctk.CTkFrame(self.h_inner, fg_color="#f3f3f3",
|
||||||
|
height=ROW_H, width=self.total_w)
|
||||||
|
row.pack(fill="x", expand=False)
|
||||||
|
row.pack_propagate(False)
|
||||||
|
|
||||||
|
for col in self.columns:
|
||||||
|
holder = ctk.CTkFrame(
|
||||||
|
row, fg_color="#f3f3f3",
|
||||||
|
width=col.width, height=ROW_H,
|
||||||
|
border_width=1, border_color=self.GRID_COLOR
|
||||||
|
)
|
||||||
|
holder.pack(side="left", fill="y")
|
||||||
|
holder.pack_propagate(False)
|
||||||
|
|
||||||
|
lbl = ctk.CTkLabel(holder, text=col.title, anchor="w")
|
||||||
|
lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY)
|
||||||
|
|
||||||
|
self.h_inner.configure(width=self.total_w, height=ROW_H)
|
||||||
|
self.h_canvas.configure(scrollregion=(0,0,self.total_w,ROW_H))
|
||||||
|
|
||||||
|
def _update_body_width(self):
|
||||||
|
self.b_canvas.itemconfigure(self.body_window, width=self.total_w)
|
||||||
|
sr = self.b_canvas.bbox("all")
|
||||||
|
if sr:
|
||||||
|
self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3]))
|
||||||
|
else:
|
||||||
|
self.b_canvas.configure(scrollregion=(0,0,self.total_w,0))
|
||||||
|
|
||||||
|
def _on_body_configure(self):
|
||||||
|
self._update_body_width()
|
||||||
|
self._sync_header_width()
|
||||||
|
|
||||||
|
def _sync_header_width(self):
|
||||||
|
first, _ = self.b_canvas.xview()
|
||||||
|
self.h_canvas.xview_moveto(first)
|
||||||
|
|
||||||
|
def _xscroll_both(self, *args):
|
||||||
|
self.h_canvas.xview(*args)
|
||||||
|
self.b_canvas.xview(*args)
|
||||||
|
|
||||||
|
def _xscroll_set_both(self, first, last):
|
||||||
|
self.h_canvas.xview_moveto(first)
|
||||||
|
self.xbar.set(first, last)
|
||||||
|
|
||||||
|
def clear_rows(self):
|
||||||
|
for w in self.b_inner.winfo_children():
|
||||||
|
w.destroy()
|
||||||
|
self._update_body_width()
|
||||||
|
|
||||||
|
def add_row(
|
||||||
|
self,
|
||||||
|
values: List[str],
|
||||||
|
row_index: int,
|
||||||
|
anchors: Optional[List[str]] = None,
|
||||||
|
checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None,
|
||||||
|
):
|
||||||
|
row = ctk.CTkFrame(self.b_inner, fg_color="transparent",
|
||||||
|
height=ROW_H, width=self.total_w)
|
||||||
|
row.pack(fill="x", expand=False)
|
||||||
|
row.pack_propagate(False)
|
||||||
|
|
||||||
|
for i, col in enumerate(self.columns):
|
||||||
|
holder = ctk.CTkFrame(
|
||||||
|
row, fg_color="transparent",
|
||||||
|
width=col.width, height=ROW_H,
|
||||||
|
border_width=1, border_color=self.GRID_COLOR
|
||||||
|
)
|
||||||
|
holder.pack(side="left", fill="y")
|
||||||
|
holder.pack_propagate(False)
|
||||||
|
|
||||||
|
if col.key == "__check__":
|
||||||
|
if checkbox_builder:
|
||||||
|
cb = checkbox_builder(holder)
|
||||||
|
cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w")
|
||||||
|
else:
|
||||||
|
ctk.CTkLabel(holder, text="").pack(fill="both")
|
||||||
|
else:
|
||||||
|
anchor = (anchors[i] if anchors else col.anchor)
|
||||||
|
ctk.CTkLabel(holder, text=values[i], anchor=anchor).pack(
|
||||||
|
fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY
|
||||||
|
)
|
||||||
|
|
||||||
|
self._update_body_width()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- PL row model --------------------
|
||||||
|
class PLRow:
|
||||||
|
def __init__(self, pl: Dict[str, Any], on_check):
|
||||||
|
self.pl = pl
|
||||||
|
self.var = ctk.BooleanVar(value=False)
|
||||||
|
self._callback = on_check
|
||||||
|
def is_checked(self) -> bool: return self.var.get()
|
||||||
|
def set_checked(self, val: bool): self.var.set(val)
|
||||||
|
def build_checkbox(self, parent) -> ctk.CTkCheckBox:
|
||||||
|
return ctk.CTkCheckBox(parent, text="", variable=self.var,
|
||||||
|
command=lambda: self._callback(self, self.var.get()))
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- main frame (no-flicker + UX tuning + spinner) --------------------
|
||||||
|
class GestionePickingListFrame(ctk.CTkFrame):
|
||||||
|
def __init__(self, master, *, db_client=None, conn_str=None):
|
||||||
|
super().__init__(master)
|
||||||
|
self.db_client = db_client or _get_db_singleton(get_global_loop(), conn_str)
|
||||||
|
self.runner = AsyncRunner(self) # runner condiviso (usa loop globale)
|
||||||
|
self.busy = BusyOverlay(self) # overlay collaudato
|
||||||
|
|
||||||
|
self.rows_models: list[PLRow] = []
|
||||||
|
self._detail_cache: Dict[Any, list] = {}
|
||||||
|
self.detail_doc = None
|
||||||
|
|
||||||
|
self._first_loading: bool = False # flag per cursore d'attesa solo al primo load
|
||||||
|
|
||||||
|
self._build_layout()
|
||||||
|
# 🔇 Niente reload immediato: carichiamo quando la finestra è idle (= già resa)
|
||||||
|
self.after_idle(self._first_show)
|
||||||
|
|
||||||
|
def _first_show(self):
|
||||||
|
"""Chiamato a finestra già resa → evitiamo sfarfallio del primo paint e mostriamo wait-cursor."""
|
||||||
|
self._first_loading = True
|
||||||
|
try:
|
||||||
|
self.winfo_toplevel().configure(cursor="watch")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# spinner inizia
|
||||||
|
self.spinner.start(" Carico…")
|
||||||
|
self.reload_from_db(first=True)
|
||||||
|
|
||||||
|
# ---------- UI ----------
|
||||||
|
def _build_layout(self):
|
||||||
|
for r in (1, 3): self.grid_rowconfigure(r, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
top = ctk.CTkFrame(self)
|
||||||
|
top.grid(row=0, column=0, sticky="ew", padx=10, pady=(8,4))
|
||||||
|
for i, (text, cmd) in enumerate([
|
||||||
|
("Ricarica", self.reload_from_db),
|
||||||
|
("Prenota", self.on_prenota),
|
||||||
|
("S-prenota", self.on_sprenota),
|
||||||
|
("Esporta XLSX", self.on_export)
|
||||||
|
]):
|
||||||
|
ctk.CTkButton(top, text=text, command=cmd).grid(row=0, column=i, padx=6)
|
||||||
|
|
||||||
|
# --- micro spinner a destra della toolbar ---
|
||||||
|
self.spinner = ToolbarSpinner(top)
|
||||||
|
self.spinner.widget().grid(row=0, column=10, padx=(8,0)) # largo spazio a destra
|
||||||
|
|
||||||
|
self.pl_table = ScrollTable(self, PL_COLS)
|
||||||
|
self.pl_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=(4,8))
|
||||||
|
|
||||||
|
self.det_table = ScrollTable(self, DET_COLS)
|
||||||
|
self.det_table.grid(row=3, column=0, sticky="nsew", padx=10, pady=(4,10))
|
||||||
|
|
||||||
|
self._draw_details_hint()
|
||||||
|
|
||||||
|
def _draw_details_hint(self):
|
||||||
|
self.det_table.clear_rows()
|
||||||
|
self.det_table.add_row(
|
||||||
|
values=["", "", "", "Seleziona una Picking List per vedere le UDC…", "", ""],
|
||||||
|
row_index=0,
|
||||||
|
anchors=["w"]*6
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_row_colors(self, rows: List[Dict[str, Any]]):
|
||||||
|
"""Colorazione differita (after_idle) per evitare micro-jank durante l'inserimento righe."""
|
||||||
|
try:
|
||||||
|
for idx, d in enumerate(rows):
|
||||||
|
row_widget = self.pl_table.b_inner.winfo_children()[idx]
|
||||||
|
if int(d.get("IDStato") or 0) == 1:
|
||||||
|
row_widget.configure(fg_color="#ffe6f2") # rosa tenue
|
||||||
|
else:
|
||||||
|
row_widget.configure(fg_color="transparent")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _refresh_mid_rows(self, rows: List[Dict[str, Any]]):
|
||||||
|
self.pl_table.clear_rows()
|
||||||
|
self.rows_models.clear()
|
||||||
|
|
||||||
|
for r, d in enumerate(rows):
|
||||||
|
model = PLRow(d, self.on_row_checked)
|
||||||
|
self.rows_models.append(model)
|
||||||
|
values = [
|
||||||
|
"", # checkbox
|
||||||
|
_s(d.get("Documento")),
|
||||||
|
_s(d.get("NAZIONE")),
|
||||||
|
_s(d.get("Stato")),
|
||||||
|
_s(d.get("IDStato")), # nuova colonna visibile
|
||||||
|
_s(d.get("Pallet")),
|
||||||
|
_s(d.get("Lotto")),
|
||||||
|
_s(d.get("Articolo")),
|
||||||
|
_s(d.get("Qta")),
|
||||||
|
]
|
||||||
|
self.pl_table.add_row(
|
||||||
|
values=values,
|
||||||
|
row_index=r,
|
||||||
|
anchors=[c.anchor for c in PL_COLS],
|
||||||
|
checkbox_builder=model.build_checkbox
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🎯 Colora dopo che la UI è resa → no balzi visivi
|
||||||
|
self.after_idle(lambda: self._apply_row_colors(rows))
|
||||||
|
|
||||||
|
# ----- helpers -----
|
||||||
|
def _get_selected_model(self) -> Optional[PLRow]:
|
||||||
|
for m in self.rows_models:
|
||||||
|
if m.is_checked():
|
||||||
|
return m
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _recolor_row_by_documento(self, documento: str, idstato: int):
|
||||||
|
"""Aggiorna colore riga e cella IDStato per il Documento indicato."""
|
||||||
|
for idx, m in enumerate(self.rows_models):
|
||||||
|
if _s(m.pl.get("Documento")) == _s(documento):
|
||||||
|
m.pl["IDStato"] = idstato
|
||||||
|
def _paint():
|
||||||
|
try:
|
||||||
|
row_widget = self.pl_table.b_inner.winfo_children()[idx]
|
||||||
|
row_widget.configure(fg_color="#ffe6f2" if idstato == 1 else "transparent")
|
||||||
|
row_children = row_widget.winfo_children()
|
||||||
|
if len(row_children) >= 5:
|
||||||
|
holder = row_children[4]
|
||||||
|
if holder.winfo_children():
|
||||||
|
lbl = holder.winfo_children()[0]
|
||||||
|
if hasattr(lbl, "configure"):
|
||||||
|
lbl.configure(text=str(idstato))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# differisci la colorazione (smooth)
|
||||||
|
self.after_idle(_paint)
|
||||||
|
break
|
||||||
|
|
||||||
|
def _reselect_documento_after_reload(self, documento: str):
|
||||||
|
"""(Opzionale) Dopo un reload DB, riseleziona la PL con lo stesso Documento."""
|
||||||
|
for m in self.rows_models:
|
||||||
|
if _s(m.pl.get("Documento")) == _s(documento):
|
||||||
|
m.set_checked(True)
|
||||||
|
self.on_row_checked(m, True)
|
||||||
|
break
|
||||||
|
|
||||||
|
# ----- eventi -----
|
||||||
|
def on_row_checked(self, model: PLRow, is_checked: bool):
|
||||||
|
# selezione esclusiva
|
||||||
|
if is_checked:
|
||||||
|
for m in self.rows_models:
|
||||||
|
if m is not model and m.is_checked():
|
||||||
|
m.set_checked(False)
|
||||||
|
|
||||||
|
self.detail_doc = model.pl.get("Documento")
|
||||||
|
self.spinner.start(" Carico dettagli…") # spinner ON
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc})
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
self.spinner.stop() # spinner OFF
|
||||||
|
self._detail_cache[self.detail_doc] = _rows_to_dicts(res)
|
||||||
|
# differisci il render dei dettagli (più fluido)
|
||||||
|
self.after_idle(self._refresh_details)
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
self.spinner.stop()
|
||||||
|
messagebox.showerror("DB", f"Errore nel caricamento dettagli:\n{ex}")
|
||||||
|
|
||||||
|
self.runner.run(
|
||||||
|
_job(),
|
||||||
|
on_success=_ok,
|
||||||
|
on_error=_err,
|
||||||
|
busy=self.busy,
|
||||||
|
message=f"Carico UDC per Documento {self.detail_doc}…"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not any(m.is_checked() for m in self.rows_models):
|
||||||
|
self.detail_doc = None
|
||||||
|
self._refresh_details()
|
||||||
|
|
||||||
|
# ----- load PL -----
|
||||||
|
def reload_from_db(self, first: bool = False):
|
||||||
|
self.spinner.start(" Carico…") # spinner ON
|
||||||
|
async def _job():
|
||||||
|
return await self.db_client.query_json(SQL_PL, {})
|
||||||
|
def _on_success(res):
|
||||||
|
rows = _rows_to_dicts(res)
|
||||||
|
self._refresh_mid_rows(rows)
|
||||||
|
self.spinner.stop() # spinner OFF
|
||||||
|
# se era il primo load, ripristina il cursore standard
|
||||||
|
if self._first_loading:
|
||||||
|
try:
|
||||||
|
self.winfo_toplevel().configure(cursor="")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._first_loading = False
|
||||||
|
def _on_error(ex):
|
||||||
|
self.spinner.stop()
|
||||||
|
if self._first_loading:
|
||||||
|
try:
|
||||||
|
self.winfo_toplevel().configure(cursor="")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._first_loading = False
|
||||||
|
messagebox.showerror("DB", f"Errore nel caricamento:\n{ex}")
|
||||||
|
|
||||||
|
self.runner.run(
|
||||||
|
_job(),
|
||||||
|
on_success=_on_success,
|
||||||
|
on_error=_on_error,
|
||||||
|
busy=self.busy,
|
||||||
|
message="Caricamento Picking List…" if first else "Aggiornamento…"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_details(self):
|
||||||
|
self.det_table.clear_rows()
|
||||||
|
if not self.detail_doc:
|
||||||
|
self._draw_details_hint()
|
||||||
|
return
|
||||||
|
|
||||||
|
rows = self._detail_cache.get(self.detail_doc, [])
|
||||||
|
if not rows:
|
||||||
|
self.det_table.add_row(values=["", "", "", "Nessuna UDC trovata.", "", ""],
|
||||||
|
row_index=0, anchors=["w"]*6)
|
||||||
|
return
|
||||||
|
|
||||||
|
for r, d in enumerate(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]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- azioni -----
|
||||||
|
def on_prenota(self):
|
||||||
|
model = self._get_selected_model()
|
||||||
|
if not model:
|
||||||
|
messagebox.showinfo("Prenota", "Seleziona una Picking List (checkbox) prima di prenotare.")
|
||||||
|
return
|
||||||
|
|
||||||
|
documento = _s(model.pl.get("Documento"))
|
||||||
|
current = int(model.pl.get("IDStato") or 0)
|
||||||
|
desired = 1
|
||||||
|
if current == desired:
|
||||||
|
messagebox.showinfo("Prenota", f"La Picking List {documento} è già prenotata.")
|
||||||
|
return
|
||||||
|
|
||||||
|
id_operatore = 1 # TODO: recupera dal contesto reale
|
||||||
|
self.spinner.start(" Prenoto…")
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento)
|
||||||
|
|
||||||
|
def _ok(res: SPResult):
|
||||||
|
self.spinner.stop()
|
||||||
|
if res and res.rc == 0:
|
||||||
|
self._recolor_row_by_documento(documento, desired)
|
||||||
|
else:
|
||||||
|
msg = (res.message if res else "Errore sconosciuto")
|
||||||
|
messagebox.showerror("Prenota", f"Operazione non riuscita:\n{msg}")
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
self.spinner.stop()
|
||||||
|
messagebox.showerror("Prenota", f"Errore:\n{ex}")
|
||||||
|
|
||||||
|
self.runner.run(
|
||||||
|
_job(),
|
||||||
|
on_success=_ok,
|
||||||
|
on_error=_err,
|
||||||
|
busy=self.busy,
|
||||||
|
message=f"Prenoto la Picking List {documento}…"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_sprenota(self):
|
||||||
|
model = self._get_selected_model()
|
||||||
|
if not model:
|
||||||
|
messagebox.showinfo("S-prenota", "Seleziona una Picking List (checkbox) prima di s-prenotare.")
|
||||||
|
return
|
||||||
|
|
||||||
|
documento = _s(model.pl.get("Documento"))
|
||||||
|
current = int(model.pl.get("IDStato") or 0)
|
||||||
|
desired = 0
|
||||||
|
if current == desired:
|
||||||
|
messagebox.showinfo("S-prenota", f"La Picking List {documento} è già NON prenotata.")
|
||||||
|
return
|
||||||
|
|
||||||
|
id_operatore = 1 # TODO: recupera dal contesto reale
|
||||||
|
self.spinner.start(" S-prenoto…")
|
||||||
|
|
||||||
|
async def _job():
|
||||||
|
return await sp_xExePackingListPallet_async(self.db_client, id_operatore, documento)
|
||||||
|
|
||||||
|
def _ok(res: SPResult):
|
||||||
|
self.spinner.stop()
|
||||||
|
if res and res.rc == 0:
|
||||||
|
self._recolor_row_by_documento(documento, desired)
|
||||||
|
else:
|
||||||
|
msg = (res.message if res else "Errore sconosciuto")
|
||||||
|
messagebox.showerror("S-prenota", f"Operazione non riuscita:\n{msg}")
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
self.spinner.stop()
|
||||||
|
messagebox.showerror("S-prenota", f"Errore:\n{ex}")
|
||||||
|
|
||||||
|
self.runner.run(
|
||||||
|
_job(),
|
||||||
|
on_success=_ok,
|
||||||
|
on_error=_err,
|
||||||
|
busy=self.busy,
|
||||||
|
message=f"S-prenoto la Picking List {documento}…"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_export(self):
|
||||||
|
messagebox.showinfo("Esporta", "Stub esportazione.")
|
||||||
|
|
||||||
|
|
||||||
|
# factory per main
|
||||||
|
def create_frame(parent, *, db_client=None, conn_str=None) -> 'GestionePickingListFrame':
|
||||||
|
ctk.set_appearance_mode("light")
|
||||||
|
ctk.set_default_color_theme("green")
|
||||||
|
return GestionePickingListFrame(parent, db_client=db_client, conn_str=conn_str)
|
||||||
|
|
||||||
|
# =================== /gestione_pickinglist.py ===================
|
||||||
670
layout_window.py
Normal file
670
layout_window.py
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
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:
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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, _):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
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:
|
||||||
|
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):
|
||||||
|
if not self._try_highlight(col_txt, fila_txt):
|
||||||
|
self._toast("Cella trovata ma non mappabile a pulsante.")
|
||||||
|
|
||||||
|
# ---------------- COMMANDS ----------------
|
||||||
|
def _refresh_current(self):
|
||||||
|
if self.corsia_selezionata.get():
|
||||||
|
self._load_matrix(self.corsia_selezionata.get())
|
||||||
|
|
||||||
|
def _export_xlsx(self):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(txt)
|
||||||
|
self._toast(f"Copiato: {txt}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
# 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):
|
||||||
|
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
|
||||||
633
layout_window.py.bak_fix_bc_transparent
Normal file
633
layout_window.py.bak_fix_bc_transparent
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
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:
|
||||||
|
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):
|
||||||
|
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._build_top()
|
||||||
|
self._build_matrix_host()
|
||||||
|
self._build_stats()
|
||||||
|
|
||||||
|
self._load_corsie()
|
||||||
|
self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))
|
||||||
|
|
||||||
|
# ---------------- TOP BAR ----------------
|
||||||
|
def _build_top(self):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
if state == 0:
|
||||||
|
btn.configure(
|
||||||
|
fg_color=COLOR_EMPTY, hover_color="#9A9A9A",
|
||||||
|
text_color=FG_DARK, border_width=0, border_color="transparent"
|
||||||
|
)
|
||||||
|
elif state == 1:
|
||||||
|
btn.configure(
|
||||||
|
fg_color=COLOR_FULL, hover_color="#E69500",
|
||||||
|
text_color=FG_DARK, border_width=0, border_color="transparent"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
btn.configure(
|
||||||
|
fg_color=COLOR_DOUBLE, hover_color="#B22222",
|
||||||
|
text_color=FG_LIGHT, border_width=0, border_color="transparent"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear_highlight(self):
|
||||||
|
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, border_color="transparent")
|
||||||
|
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, border_color="transparent")
|
||||||
|
# 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):
|
||||||
|
# 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 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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, _):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
# 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 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):
|
||||||
|
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 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 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:
|
||||||
|
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):
|
||||||
|
if not self._try_highlight(col_txt, fila_txt):
|
||||||
|
self._toast("Cella trovata ma non mappabile a pulsante.")
|
||||||
|
|
||||||
|
# ---------------- COMMANDS ----------------
|
||||||
|
def _refresh_current(self):
|
||||||
|
if self.corsia_selezionata.get():
|
||||||
|
self._load_matrix(self.corsia_selezionata.get())
|
||||||
|
|
||||||
|
def _export_xlsx(self):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(txt)
|
||||||
|
self._toast(f"Copiato: {txt}")
|
||||||
|
|
||||||
|
|
||||||
|
def open_layout_window(parent, db_app):
|
||||||
|
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
|
||||||
632
layout_window.py.bak_perf
Normal file
632
layout_window.py.bak_perf
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
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:
|
||||||
|
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):
|
||||||
|
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._build_top()
|
||||||
|
self._build_matrix_host()
|
||||||
|
self._build_stats()
|
||||||
|
|
||||||
|
self._load_corsie()
|
||||||
|
self.bind("<Configure>", lambda e: self.after_idle(self._refresh_stats))
|
||||||
|
|
||||||
|
# ---------------- TOP BAR ----------------
|
||||||
|
def _build_top(self):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
# 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 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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, _):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
# 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 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):
|
||||||
|
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 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 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:
|
||||||
|
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):
|
||||||
|
if not self._try_highlight(col_txt, fila_txt):
|
||||||
|
self._toast("Cella trovata ma non mappabile a pulsante.")
|
||||||
|
|
||||||
|
# ---------------- COMMANDS ----------------
|
||||||
|
def _refresh_current(self):
|
||||||
|
if self.corsia_selezionata.get():
|
||||||
|
self._load_matrix(self.corsia_selezionata.get())
|
||||||
|
|
||||||
|
def _export_xlsx(self):
|
||||||
|
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):
|
||||||
|
# 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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(txt)
|
||||||
|
self._toast(f"Copiato: {txt}")
|
||||||
|
|
||||||
|
|
||||||
|
def open_layout_window(parent, db_app):
|
||||||
|
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
|
||||||
156
main.py
Normal file
156
main.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import tkinter as tk
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
from async_msssql_query import AsyncMSSQLClient, make_mssql_dsn
|
||||||
|
from async_loop_singleton import get_global_loop
|
||||||
|
|
||||||
|
from layout_window import open_layout_window
|
||||||
|
from view_celle_multiple import open_celle_multiple_window
|
||||||
|
from reset_corsie import open_reset_corsie_window
|
||||||
|
from search_pallets import open_search_window
|
||||||
|
|
||||||
|
# Try factory, else frame, else app (senza passare conn_str all'App)
|
||||||
|
try:
|
||||||
|
from gestione_pickinglist import create_frame as create_pickinglist_frame
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from gestione_pickinglist import GestionePickingListFrame as _PLFrame
|
||||||
|
import customtkinter as ctk
|
||||||
|
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
|
||||||
|
ctk.set_appearance_mode("light")
|
||||||
|
ctk.set_default_color_theme("green")
|
||||||
|
return _PLFrame(parent, db_client=db_client, conn_str=conn_str)
|
||||||
|
except Exception:
|
||||||
|
# Ultimo fallback: alcune versioni espongono solo la App e NON accettano conn_str
|
||||||
|
# Ultimo fallback: alcune versioni espongono solo la App e NON accettano parametri
|
||||||
|
from gestione_pickinglist import GestionePickingListApp as _PLApp
|
||||||
|
def create_pickinglist_frame(parent, db_client=None, conn_str=None):
|
||||||
|
app = _PLApp() # <-- niente parametri qui
|
||||||
|
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()
|
||||||
|
asyncio.set_event_loop(_loop)
|
||||||
|
|
||||||
|
# --- DPI tracker compatibility ---
|
||||||
|
def _noop(*args, **kwargs):
|
||||||
|
return None
|
||||||
|
if not hasattr(tk.Toplevel, "block_update_dimensions_event"):
|
||||||
|
tk.Toplevel.block_update_dimensions_event = _noop # type: ignore[attr-defined]
|
||||||
|
if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
|
||||||
|
tk.Toplevel.unblock_update_dimensions_event = _noop # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
dsn_app = make_mssql_dsn(server=SERVER, database=DBNAME, user=USER, password=PASSWORD)
|
||||||
|
db_app = AsyncMSSQLClient(dsn_app)
|
||||||
|
|
||||||
|
|
||||||
|
def open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient):
|
||||||
|
win = ctk.CTkToplevel(parent)
|
||||||
|
win.title("Gestione Picking List")
|
||||||
|
win.geometry("1200x700+0+100")
|
||||||
|
win.minsize(1000, 560)
|
||||||
|
|
||||||
|
# 1) tieni la toplevel fuori scena mentre componi
|
||||||
|
try:
|
||||||
|
win.withdraw()
|
||||||
|
# opzionale: rendila invisibile anche se il WM la “intr intravede”
|
||||||
|
win.attributes("-alpha", 0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2) costruisci tutto il contenuto
|
||||||
|
frame = create_pickinglist_frame(win, db_client=db_client)
|
||||||
|
try:
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3) quando è pronta, mostra "a scatto" davanti, senza topmost
|
||||||
|
try:
|
||||||
|
win.update_idletasks()
|
||||||
|
try:
|
||||||
|
win.transient(parent) # z-order legato alla main
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
win.deiconify()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
win.lift()
|
||||||
|
try:
|
||||||
|
win.focus_force()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# ripristina opacità
|
||||||
|
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):
|
||||||
|
def __init__(self):
|
||||||
|
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():
|
||||||
|
try:
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
|
||||||
|
try:
|
||||||
|
fut.result(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
self.protocol("WM_DELETE_WINDOW", _on_close)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ctk.set_appearance_mode("light")
|
||||||
|
ctk.set_default_color_theme("green")
|
||||||
|
Launcher().mainloop()
|
||||||
62
pickinglist.sql
Normal file
62
pickinglist.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
SELECT * FROM vViewMappaturaDescrizioneCorsia WHERE ( Area > 0) ORDER BY Area Desc
|
||||||
|
|
||||||
|
SELECT * FROM Celle WHERE ( ID > 0) ORDER BY ID Desc
|
||||||
|
|
||||||
|
SELECT TOP 1000 [mc_X] ,[minFila] ,[maxFila] ,[minColonna] ,[maxColonna] ,[mc_Numero_Magazzino] ,[mc_Numero_Area] FROM vViewMappaturaPosizCorsia WHERE [mc_Numero_Area] =1
|
||||||
|
|
||||||
|
|
||||||
|
SELECT CASE WHEN Nota = 'ASC' THEN 0 ELSE CASE WHEN Nota = 'DESC' THEN 1 END END AS Nota FROM MagLayout WHERE (IDArea = 1)
|
||||||
|
|
||||||
|
SELECT ID, Descrizione FROM Magazzini ORDER BY Descrizione
|
||||||
|
|
||||||
|
SELECT ID, Descrizione FROM Celle ORDER BY Descrizione
|
||||||
|
|
||||||
|
SELECT [ID],[CorsiaDescrizione] FROM vViewMappaturaDescrizioneCorsia
|
||||||
|
|
||||||
|
|
||||||
|
SELECT COUNT(DISTINCT Pallet) AS Pallet, COUNT(DISTINCT Lotto) AS Lotto, COUNT(DISTINCT Articolo) AS Articolo, COUNT(DISTINCT Descrizione) AS Descrizione, SUM(Qta) AS Qta, Documento, CodNazione, NAZIONE, Stato, MAX(PalletCella) AS PalletCella, MAX(Magazzino) AS Magazzino, MAX(Area) AS Area, MAX(Cella) AS Cella, MIN(Ordinamento) AS Ordinamento, MAX(IDStato) AS IDStato FROM dbo.XMag_ViewPackingList GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||||
|
|
||||||
|
SELECT * FROM vViewPackingListRestante WHERE Documento = 237 ORDER BY Ordinamento
|
||||||
|
SELECT * FROM ViewPackingListRestante WHERE Documento = 246 ORDER BY Ordinamento
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
USE master;
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- (facoltativo) terminare connessioni attive
|
||||||
|
ALTER DATABASE SAMA1 SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Ripristino dal tuo snapshot
|
||||||
|
RESTORE DATABASE SAMA1
|
||||||
|
FROM DATABASE_SNAPSHOT = 'SAMA1_SNAP_20251014_112623';
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Riporta il DB in multi-user
|
||||||
|
ALTER DATABASE SAMA1 SET MULTI_USER;
|
||||||
|
GO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
SELECT @@SERVERNAME AS server_name, DB_NAME() AS database_name;
|
||||||
|
|
||||||
|
SELECT name, create_date
|
||||||
|
FROM sys.databases
|
||||||
|
WHERE source_database_id IS NOT NULL;
|
||||||
|
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS righe FROM dbo.XMag_ViewPackingList;
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS righe FROM dbo.ViewPackingListRestante;
|
||||||
|
|
||||||
|
SELECT Documento, Stato, IDStato, NAZIONE, COUNT(*) AS righe
|
||||||
|
FROM dbo.ViewPackingListRestante
|
||||||
|
WHERE Documento IN (240,241)
|
||||||
|
GROUP BY Documento, Stato, IDStato, NAZIONE;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
EXEC sp_refreshview N'dbo.XMag_ViewPackingList';
|
||||||
|
EXEC sp_refreshview N'dbo.ViewPackingListRestante';
|
||||||
189
prenota_sprenota_sql.py
Normal file
189
prenota_sprenota_sql.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SPResult:
|
||||||
|
rc: int = 0 # equivalente a @RC OUTPUT
|
||||||
|
message: Optional[str] = "" # eventuale messaggio/errore
|
||||||
|
id_result: Optional[int] = None # ID del record inserito in LogPackingList
|
||||||
|
|
||||||
|
|
||||||
|
# --- helpers per il client async (senza conoscere l'API esatta forniamo fallback robusti) ---
|
||||||
|
async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Ritorna la prima colonna della prima riga, oppure None.
|
||||||
|
Tenta prima query_json(...), poi altri metodi comuni.
|
||||||
|
"""
|
||||||
|
if hasattr(db, "query_json"):
|
||||||
|
res = await db.query_json(sql, params)
|
||||||
|
# res può essere una lista di dict o un payload con rows/columns
|
||||||
|
if isinstance(res, list) and res:
|
||||||
|
row0 = res[0]
|
||||||
|
if isinstance(row0, dict):
|
||||||
|
# prima colonna disponibile
|
||||||
|
return next(iter(row0.values()), None)
|
||||||
|
elif isinstance(res, dict):
|
||||||
|
rows = None
|
||||||
|
for k in ("rows", "data", "result", "records"):
|
||||||
|
if k in res and isinstance(res[k], list):
|
||||||
|
rows = res[k]
|
||||||
|
break
|
||||||
|
if rows:
|
||||||
|
r0 = rows[0]
|
||||||
|
if isinstance(r0, dict):
|
||||||
|
return next(iter(r0.values()), None)
|
||||||
|
if isinstance(r0, (list, tuple)) and r0:
|
||||||
|
return r0[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# fallback: altri metodi (se esistono)
|
||||||
|
if hasattr(db, "query_value"):
|
||||||
|
return await db.query_value(sql, params)
|
||||||
|
if hasattr(db, "scalar"):
|
||||||
|
return await db.scalar(sql, params)
|
||||||
|
raise RuntimeError("Il client DB non espone query_json/query_value/scalar")
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Ritorna una lista di dict {col:val}."""
|
||||||
|
if hasattr(db, "query_json"):
|
||||||
|
res = await db.query_json(sql, params)
|
||||||
|
if res is None:
|
||||||
|
return []
|
||||||
|
if isinstance(res, list):
|
||||||
|
return res if res and isinstance(res[0], dict) else []
|
||||||
|
if isinstance(res, dict):
|
||||||
|
for k in ("rows", "data", "result", "records"):
|
||||||
|
if k in res and isinstance(res[k], list):
|
||||||
|
rows = res[k]
|
||||||
|
if rows and isinstance(rows[0], dict):
|
||||||
|
return rows
|
||||||
|
cols = res.get("columns") or res.get("cols") or []
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
if isinstance(r, (list, tuple)) and cols:
|
||||||
|
out.append({ (cols[i] if i < len(cols) else f"c{i}") : r[i]
|
||||||
|
for i in range(min(len(cols), len(r))) })
|
||||||
|
return out
|
||||||
|
return []
|
||||||
|
# fallback
|
||||||
|
if hasattr(db, "fetch_all"):
|
||||||
|
return await db.fetch_all(sql, params)
|
||||||
|
raise RuntimeError("Il client DB non espone query_json/fetch_all")
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute(db, sql: str, params: Dict[str, Any]) -> int:
|
||||||
|
"""
|
||||||
|
Esegue DML e ritorna rowcount (se disponibile).
|
||||||
|
Prova .execute / .exec / .execute_non_query / altrimenti usa query_json.
|
||||||
|
"""
|
||||||
|
for name in ("execute", "exec", "execute_non_query"):
|
||||||
|
if hasattr(db, name):
|
||||||
|
rc = await getattr(db, name)(sql, params)
|
||||||
|
# alcuni client ritornano None, altri rowcount, altri payload
|
||||||
|
if isinstance(rc, int):
|
||||||
|
return rc
|
||||||
|
return 0
|
||||||
|
# fallback rozzo: molti back-end accettano anche DML in query_json
|
||||||
|
if hasattr(db, "query_json"):
|
||||||
|
await db.query_json(sql, params)
|
||||||
|
return 0
|
||||||
|
raise RuntimeError("Il client DB non espone metodi di esecuzione DML noti")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Procedura portata in async, usando il client DB passato dall'app ---
|
||||||
|
async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) -> SPResult:
|
||||||
|
"""
|
||||||
|
Porting asincrono di [dbo].[sp_xExePackingListPallet] usando il client DB già aperto dall'app.
|
||||||
|
Logica:
|
||||||
|
1) Recupera LOGIN operatore
|
||||||
|
2) Elenca le celle (DISTINCT Cella da XMag_ViewPackingList per Documento)
|
||||||
|
3) Per ogni cella: leggi IDStato e toggla 0<->1 + aggiorna ModUtente/ModDataOra
|
||||||
|
4) Description = TOP 1 NAZIONE per Documento
|
||||||
|
5) Inserisci LogPackingList(Code=Documento, Description, IDInsUser=IDOperatore, InsDateTime=GETDATE())
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1) LOGIN operatore (se manca, prosegue come da SP originaria)
|
||||||
|
nominativo = await _query_one_value(
|
||||||
|
db,
|
||||||
|
"SELECT LOGIN FROM Operatori WHERE id = :IDOperatore",
|
||||||
|
{"IDOperatore": IDOperatore}
|
||||||
|
) or ""
|
||||||
|
|
||||||
|
# 2) Celle da trattare
|
||||||
|
celle = await _query_all(
|
||||||
|
db,
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT Cella
|
||||||
|
FROM dbo.XMag_ViewPackingList
|
||||||
|
WHERE Documento = :Documento
|
||||||
|
""",
|
||||||
|
{"Documento": Documento}
|
||||||
|
)
|
||||||
|
id_celle = [r.get("Cella") for r in celle if "Cella" in r]
|
||||||
|
|
||||||
|
# 3) Toggle stato per ogni cella
|
||||||
|
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
|
||||||
|
SET IDStato = 1,
|
||||||
|
ModUtente = :N,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = :IDC
|
||||||
|
""",
|
||||||
|
{"N": nominativo, "IDC": id_cella}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await _execute(
|
||||||
|
db,
|
||||||
|
"""
|
||||||
|
UPDATE Celle
|
||||||
|
SET IDStato = 0,
|
||||||
|
ModUtente = :N,
|
||||||
|
ModDataOra = GETDATE()
|
||||||
|
WHERE ID = :IDC
|
||||||
|
""",
|
||||||
|
{"N": nominativo, "IDC": id_cella}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4) Description = NAZIONE (TOP 1)
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5) LogPackingList
|
||||||
|
await _execute(
|
||||||
|
db,
|
||||||
|
"""
|
||||||
|
INSERT INTO dbo.LogPackingList (Code, Description, IDInsUser, InsDateTime)
|
||||||
|
VALUES (:Code, :Descr, :IDInsUser, GETDATE());
|
||||||
|
""",
|
||||||
|
{"Code": Documento, "Descr": description, "IDInsUser": IDOperatore}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Se vuoi proprio l'ID appena inserito:
|
||||||
|
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 e:
|
||||||
|
return SPResult(rc=-1, message=str(e), id_result=None)
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "warehouse"
|
||||||
|
version = "0.0.1"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"sqlalchemy[asyncio]>=2.0",
|
||||||
|
"aioodbc>=0.3.3",
|
||||||
|
# "orjson>=3.9" # opzionale: il tuo codice fa fallback su json puro
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest", "pytest-cov", "mypy", "black", "flake8"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-q --maxfail=1"
|
||||||
|
pythonpath = ["."]
|
||||||
50
readme.txt
Normal file
50
readme.txt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
-- dbo.XMag_DettaglioPallet source
|
||||||
|
|
||||||
|
ALTER VIEW [dbo].[XMag_DettaglioPallet]
|
||||||
|
AS
|
||||||
|
SELECT dbo.MagazziniPallet.ID, dbo.MagazziniPallet.Tipo, dbo.MagazziniPallet.IDRiferimento, dbo.MagazziniPallet.NumeroPallet,
|
||||||
|
dbo.MagazziniPallet.IDMagazzino, dbo.MagazziniPallet.IDArea, dbo.MagazziniPallet.IDCella, dbo.MagazziniPallet.DataMagazzino,
|
||||||
|
dbo.MagazziniPallet.PesoUnitario, dbo.MagazziniPallet.Tara, dbo.MagazziniPallet.Attributo, dbo.Celle.IDStato
|
||||||
|
FROM dbo.MagazziniPallet INNER JOIN
|
||||||
|
dbo.Celle ON dbo.MagazziniPallet.IDCella = dbo.Celle.ID
|
||||||
|
WHERE (dbo.MagazziniPallet.Tipo = 'V')
|
||||||
|
UNION ALL
|
||||||
|
SELECT MagazziniPallet_1.ID, MagazziniPallet_1.Tipo, MagazziniPallet_1.IDRiferimento, MagazziniPallet_1.NumeroPallet, MagazziniPallet_1.IDMagazzino,
|
||||||
|
MagazziniPallet_1.IDArea, MagazziniPallet_1.IDCella, MagazziniPallet_1.DataMagazzino, - (1 * MagazziniPallet_1.PesoUnitario) AS PesoUnitario,
|
||||||
|
MagazziniPallet_1.Tara, MagazziniPallet_1.Attributo, Celle_1.IDStato
|
||||||
|
FROM dbo.MagazziniPallet AS MagazziniPallet_1 INNER JOIN
|
||||||
|
dbo.Celle AS Celle_1 ON MagazziniPallet_1.IDCella = Celle_1.ID
|
||||||
|
WHERE (MagazziniPallet_1.Tipo = 'P');
|
||||||
|
|
||||||
|
:commento
|
||||||
|
Dalla tabella Celle e MagazziniPallet ottengo l'insieme delle celle con tipo V = vuota e p=piena, le celle in questo caso possono essere di qualunque tipo cioè fare riferimento a pallet ancora in deposito oppure già spediti, o piene (1A.15.0e) o spedite (7G.01.01).
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW dbo.XMag_GiacenzaPallet
|
||||||
|
AS
|
||||||
|
SELECT Attributo AS BarcodePallet, NumeroPallet, IDMagazzino, IDArea, IDCella, SUM(PesoUnitario) AS Peso, Attributo AS CodiceProdotto, IDStato
|
||||||
|
FROM dbo.XMag_DettaglioPallet
|
||||||
|
GROUP BY NumeroPallet, IDMagazzino, IDArea, IDCella, Attributo, IDStato
|
||||||
|
HAVING (SUM(PesoUnitario) > 0);
|
||||||
|
commento:
|
||||||
|
Ricomprimo la vista ..dettaglio perchè capita che una udc sia stata allocata più volte sulla stessa cella, la riga in questione appare doppia se non si tiene conto della data in cui è avvenuta la lettura .SI potrebbe pensare di prendere sempre la data più alta , cioè l'ultima a parità di unità di carico.
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW [dbo].[XMag_GiacenzaPalletxUbicazioneCella]
|
||||||
|
AS
|
||||||
|
SELECT dbo.Celle.ID as IDCella, dbo.vXTracciaProdotti.Pallet, dbo.vXTracciaProdotti.Lotto, dbo.vXTracciaProdotti.Prodotto, dbo.vXTracciaProdotti.Descrizione, UPPER(REPLICATE('0',
|
||||||
|
3 - DATALENGTH(dbo.Celle.Corsia)) + dbo.Celle.Corsia + '.' + + REPLICATE('0', 2 - DATALENGTH(dbo.Celle.Colonna))
|
||||||
|
+ dbo.Celle.Colonna + '.' + REPLICATE('0', 2 - DATALENGTH(dbo.Celle.Fila)) + dbo.Celle.Fila) AS Ubicazione
|
||||||
|
FROM dbo.XMag_GiacenzaPallet
|
||||||
|
INNER JOIN
|
||||||
|
dbo.vXTracciaProdotti ON dbo.XMag_GiacenzaPallet.BarcodePallet = dbo.vXTracciaProdotti.Pallet COLLATE Latin1_General_CI_AS
|
||||||
|
LEFT OUTER JOIN
|
||||||
|
dbo.Celle ON dbo.XMag_GiacenzaPallet.IDCella = dbo.Celle.ID;
|
||||||
|
|
||||||
|
:commento
|
||||||
|
qui commbino una vista proveniente da sam in cui vado a cercare i dati della unità di carico e li combino con la tabella vista giacenzapallet, in pratica definisco dove in quale ubicazione si trova il pallet
|
||||||
|
|
||||||
|
quindi in pratica ho
|
||||||
|
|
||||||
|
tabella vista vista
|
||||||
|
magazzinipallet ---->xmag_Dettagliopallet--->Xmag_giacenzapallet
|
||||||
245
reset_corsie.py
Normal file
245
reset_corsie.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# reset_corsie.py
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, simpledialog
|
||||||
|
import customtkinter as ctk
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||||
|
|
||||||
|
# ---------------- SQL ----------------
|
||||||
|
SQL_CORSIE = """
|
||||||
|
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;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_RIEPILOGO = """
|
||||||
|
WITH C AS (
|
||||||
|
SELECT ID, LTRIM(RTRIM(Corsia)) AS Corsia,
|
||||||
|
LTRIM(RTRIM(Colonna)) AS Colonna,
|
||||||
|
LTRIM(RTRIM(Fila)) AS Fila
|
||||||
|
FROM dbo.Celle
|
||||||
|
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||||
|
AND LTRIM(RTRIM(Corsia)) = :corsia
|
||||||
|
),
|
||||||
|
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
|
||||||
|
COUNT(*) AS TotCelle,
|
||||||
|
SUM(CASE WHEN s.n>0 THEN 1 ELSE 0 END) AS CelleOccupate,
|
||||||
|
SUM(CASE WHEN s.n>1 THEN 1 ELSE 0 END) AS CelleDoppie,
|
||||||
|
SUM(COALESCE(s.n,0)) AS TotPallet
|
||||||
|
FROM C LEFT JOIN S s ON s.ID = C.ID;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_DETTAGLIO = """
|
||||||
|
WITH C AS (
|
||||||
|
SELECT ID, LTRIM(RTRIM(Corsia)) AS Corsia,
|
||||||
|
LTRIM(RTRIM(Colonna)) AS Colonna,
|
||||||
|
LTRIM(RTRIM(Fila)) AS Fila
|
||||||
|
FROM dbo.Celle
|
||||||
|
WHERE ID <> 9999 AND (DelDataOra IS NULL)
|
||||||
|
AND LTRIM(RTRIM(Corsia)) = :corsia
|
||||||
|
),
|
||||||
|
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 c.ID AS IDCella,
|
||||||
|
CONCAT(c.Corsia, '.', c.Colonna, '.', c.Fila) AS Ubicazione,
|
||||||
|
COALESCE(s.n,0) AS NumUDC
|
||||||
|
FROM C c LEFT JOIN S s ON s.ID = c.ID
|
||||||
|
WHERE COALESCE(s.n,0) > 0
|
||||||
|
ORDER BY TRY_CONVERT(int,c.Colonna), c.Colonna, TRY_CONVERT(int,c.Fila), c.Fila;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_COUNT_DELETE = """
|
||||||
|
SELECT COUNT(*) AS RowsToDelete
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN dbo.Celle c ON c.ID = mp.IDCella
|
||||||
|
WHERE c.ID <> 9999 AND LTRIM(RTRIM(c.Corsia)) = :corsia;
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_DELETE = """
|
||||||
|
DELETE mp
|
||||||
|
FROM dbo.MagazziniPallet mp
|
||||||
|
JOIN dbo.Celle c ON c.ID = mp.IDCella
|
||||||
|
WHERE c.ID <> 9999 AND LTRIM(RTRIM(c.Corsia)) = :corsia;
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ResetCorsieWindow(ctk.CTkToplevel):
|
||||||
|
"""
|
||||||
|
Finestra per:
|
||||||
|
- selezionare una corsia
|
||||||
|
- vedere riepilogo occupazione / doppie / pallet
|
||||||
|
- vedere l'elenco celle occupate
|
||||||
|
- svuotare (DELETE MagazziniPallet) tutte le celle della corsia selezionata
|
||||||
|
"""
|
||||||
|
def __init__(self, parent, db_client):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Reset Corsie — svuotamento celle per corsia")
|
||||||
|
self.geometry("1000x680")
|
||||||
|
self.minsize(880, 560)
|
||||||
|
self.resizable(True, True)
|
||||||
|
|
||||||
|
self.db = db_client
|
||||||
|
self._busy = BusyOverlay(self)
|
||||||
|
self._async = AsyncRunner(self)
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._load_corsie()
|
||||||
|
|
||||||
|
# ---------- UI ----------
|
||||||
|
def _build_ui(self):
|
||||||
|
top = ctk.CTkFrame(self); top.pack(fill="x", padx=8, pady=8)
|
||||||
|
ctk.CTkLabel(top, text="Corsia:").pack(side="left")
|
||||||
|
self.cmb = ctk.CTkComboBox(top, width=140, values=[])
|
||||||
|
self.cmb.pack(side="left", padx=(6,10))
|
||||||
|
ctk.CTkButton(top, text="Carica", command=self.refresh).pack(side="left")
|
||||||
|
ctk.CTkButton(top, text="Svuota corsia…", command=self._ask_reset).pack(side="right")
|
||||||
|
|
||||||
|
mid = ctk.CTkFrame(self); mid.pack(fill="both", expand=True, padx=8, pady=(0,8))
|
||||||
|
mid.grid_columnconfigure(0, weight=1); mid.grid_rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.tree = ttk.Treeview(mid, columns=("Ubicazione","NumUDC"), show="headings", selectmode="browse")
|
||||||
|
self.tree.heading("Ubicazione", text="Ubicazione")
|
||||||
|
self.tree.heading("NumUDC", text="UDC in cella")
|
||||||
|
self.tree.column("Ubicazione", width=240, anchor="w")
|
||||||
|
self.tree.column("NumUDC", width=120, anchor="e")
|
||||||
|
|
||||||
|
sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview)
|
||||||
|
sx = ttk.Scrollbar(mid, 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")
|
||||||
|
|
||||||
|
bottom = ctk.CTkFrame(self)
|
||||||
|
bottom.pack(fill="x", padx=8, pady=(0,8))
|
||||||
|
ctk.CTkLabel(bottom, text="Riepilogo", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=8, pady=(8,0))
|
||||||
|
|
||||||
|
g = ctk.CTkFrame(bottom)
|
||||||
|
g.pack(fill="x", padx=8, pady=8)
|
||||||
|
self.var_tot_celle = tk.StringVar(value="0")
|
||||||
|
self.var_occ = tk.StringVar(value="0")
|
||||||
|
self.var_dbl = tk.StringVar(value="0")
|
||||||
|
self.var_pallet = tk.StringVar(value="0")
|
||||||
|
|
||||||
|
def _kv(parent, label, var, col):
|
||||||
|
ctk.CTkLabel(parent, text=label, font=("Segoe UI", 9, "bold")).grid(row=0, column=col*2, sticky="w", padx=(0,6))
|
||||||
|
ctk.CTkLabel(parent, textvariable=var).grid(row=0, column=col*2+1, sticky="w", padx=(0,18))
|
||||||
|
|
||||||
|
g.grid_columnconfigure(7, weight=1)
|
||||||
|
_kv(g, "Tot. celle:", self.var_tot_celle, 0)
|
||||||
|
_kv(g, "Celle occupate:", self.var_occ, 1)
|
||||||
|
_kv(g, "Celle doppie:", self.var_dbl, 2)
|
||||||
|
_kv(g, "Tot. pallet:", self.var_pallet, 3)
|
||||||
|
|
||||||
|
# ---------- Data ----------
|
||||||
|
def _load_corsie(self):
|
||||||
|
def _ok(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
items = [r[0] for r in rows]
|
||||||
|
self.cmb.configure(values=items)
|
||||||
|
if items:
|
||||||
|
# auto 1A se presente
|
||||||
|
sel = "1A" if "1A" in items else items[0]
|
||||||
|
self.cmb.set(sel)
|
||||||
|
self.refresh()
|
||||||
|
else:
|
||||||
|
messagebox.showinfo("Info", "Nessuna corsia trovata.", parent=self)
|
||||||
|
def _err(ex):
|
||||||
|
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…")
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
corsia = self.cmb.get().strip()
|
||||||
|
if not corsia:
|
||||||
|
return
|
||||||
|
# riepilogo
|
||||||
|
def _ok_sum(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
if rows:
|
||||||
|
tot, occ, dbl, pallet = rows[0]
|
||||||
|
self.var_tot_celle.set(str(tot or 0))
|
||||||
|
self.var_occ.set(str(occ or 0))
|
||||||
|
self.var_dbl.set(str(dbl or 0))
|
||||||
|
self.var_pallet.set(str(pallet or 0))
|
||||||
|
else:
|
||||||
|
self.var_tot_celle.set("0"); self.var_occ.set("0"); self.var_dbl.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}…")
|
||||||
|
|
||||||
|
# dettaglio
|
||||||
|
def _ok_det(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
for i in self.tree.get_children(): self.tree.delete(i)
|
||||||
|
for idc, ubi, n in rows:
|
||||||
|
self.tree.insert("", "end", values=(ubi, n))
|
||||||
|
def _err_det(ex):
|
||||||
|
messagebox.showerror("Errore", f"Dettaglio fallito:\n{ex}", parent=self)
|
||||||
|
|
||||||
|
self._async.run(self.db.query_json(SQL_DETTAGLIO, {"corsia": corsia}), _ok_det, _err_det, busy=None, message=None)
|
||||||
|
|
||||||
|
# ---------- Reset ----------
|
||||||
|
def _ask_reset(self):
|
||||||
|
corsia = self.cmb.get().strip()
|
||||||
|
if not corsia:
|
||||||
|
return
|
||||||
|
# Primo: quante righe verrebbero cancellate?
|
||||||
|
def _ok_count(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
n = int(rows[0][0]) if rows else 0
|
||||||
|
if n <= 0:
|
||||||
|
messagebox.showinfo("Svuota corsia", f"Nessun pallet da rimuovere per la corsia {corsia}.", parent=self)
|
||||||
|
return
|
||||||
|
# doppia conferma
|
||||||
|
msg = (f"Verranno cancellati {n} record da MagazziniPallet per la corsia {corsia}.",
|
||||||
|
"Questa operazione è irreversibile.",
|
||||||
|
"Digitare il nome della corsia per confermare:")
|
||||||
|
confirm = simpledialog.askstring("Conferma", "\n".join(msg), parent=self)
|
||||||
|
if confirm is None:
|
||||||
|
return
|
||||||
|
if confirm.strip().upper() != corsia.upper():
|
||||||
|
messagebox.showwarning("Annullato", "Testo di conferma non corrispondente.", parent=self)
|
||||||
|
return
|
||||||
|
self._do_reset(corsia)
|
||||||
|
def _err_count(ex):
|
||||||
|
messagebox.showerror("Errore", f"Conteggio righe da cancellare 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…")
|
||||||
|
|
||||||
|
def _do_reset(self, corsia: str):
|
||||||
|
def _ok_del(_):
|
||||||
|
messagebox.showinfo("Completato", f"Corsia {corsia}: svuotamento completato.", parent=self)
|
||||||
|
self.refresh()
|
||||||
|
def _err_del(ex):
|
||||||
|
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}…")
|
||||||
|
|
||||||
|
|
||||||
|
def open_reset_corsie_window(parent, db_app):
|
||||||
|
win = ResetCorsieWindow(parent, db_app)
|
||||||
|
win.lift(); win.focus_set()
|
||||||
|
return win
|
||||||
68
riassunto_ricerca_udc_20251005.pdf
Normal file
68
riassunto_ricerca_udc_20251005.pdf
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 7 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 6 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Author (\(anonymous\)) /CreationDate (D:20251005172731+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251005172731+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1709
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gat=*@;jjj&H1Hm0iGadU(Bku9dAJL2C.[\TU.Q9Q]mO#@Lm(Sp9&>cjoW<shQ.0C8O"4d$/jXK1N2-_fb'e64UQ]!SET"'L:8BGNG6,(dDb$qk#8jZ^\[-DW*/raP/cr[K?MI4Qe6iofJ5L^>[1`N*qiBFUbb:t$ZD@Ec$Rrc:#E>K^+uO]`H_<@f%R>]SYAN9dkSI:dJ(cUE,*SE%^0`$he:F5;`W^eb4D@^V>9!LX9O&Hf@H2%Yf@!>R8'^*;WLukGdAr#Q>u-o`@`@"VE16M1<NR-iXIt0qfkAu*32d%pBlhcU%o3.GW0k6^LNUO=`P2EL<3LVJ4d4Yq&_cD<4`"'<Qo'Ym_[p[T;$+H)A*a$+6H>;YuZ(o-:h*+":B/Ybk#e6*##Bcd,69%*OtBbPWf7OqUEkie@Y+BgS`]d.rA7b-0AP&_j<ru8)Ah$ca6]?FWi%oGX:5(AbOgrjBO=_0@!,O7E.?W;O.0EoG`>;*bmRA=L.#s7qe+MJk^jiTMBcL@L"Ri>^8cc#JW(b\&oP2+nLb[hu0fkQ`48tkRO3EIoNR&K@h*B8ViJ@0Gr4K<43)Ekb?5464etE>.40O/bH4]],f/an%VUh8lchsj8W2]X67"->O_#1`--2`?Z]<NWRKTj2kgAj;Ca2RA($05kF16O,GuY*V^^S`<Y23(=O\au6F0ApXl-Ab$P)7`ZI5S`U?K(CKW$0ImjfD/.3XIb-WJnB35Xhbq\V@"93VXKE]VIP!qhXq.4P\c6U%Fg=dL7hQ-M5/)J;,16'`-f'\ZI`<>kl)4Pm;7fUW`MRnbo*+d.RFV474lEba!uPu]6?1o(iK$eRp!Lg?Ck,p;N,]1c)ABNkcc8G":n'(1;aqN]ptL$EM4FLg"DB]oe9TuiHl`DE^R$,08$nE?+HEqVm[1\P-;6bHfD>n1b\Tr0Yg'WM1s8?n'JM8;KiGA\H0lUna6N)B7:k@&]:Yite,/M5d!,b-RLX_NbV^[\U.;:mWF?aP=o%s=ppcCl9E)X?7?5oq_DO?hLgOZGHVN6FWNW[b.&HZ"CaYrR^CM8?#;,`AK2f-^b7/K4n[PK@i'>Q0?.o(2$'Ze=7ko8h9jSPWUHe0>@9.!CYSo[8XE#Y1TX-qnH!XIfSck8%_PGV>)b=3.pqLM^Ubm$:@\%E*mpC).A.o]>sl$KXV]V)!n0m8Q2'@1R@fdm?KEa&pRc7^YEZMMqR!Zq3%&8Y[e&,D$Zoe-G$8LW,u$h1pP;3.dZB6Bh^4n]F$bicJtuA>RX;U3Lhk-7qu1ehU[4iBG7D.W)e'`<j=EGn%lqi$>FR>]a`HY:(/2lN`7fRPUHLgHmjD"ihb'_R_3(bf2OcpSbMY.8godBKC"pn`t5'>J_Sagk`EjYrUPj6Yf1)NJ/WnkE'+]?P`OYUD81gAqBphrVcZT%q/F)GC5d0Fl:Q_WIV/@9N\A+rmqX-An>"Oo1C;X%ng]E;AUL*;rnFt9^FllJqBW&Q'44ZXq<1!>.;%,L^=?Mdp2+\VY1!Woqu34/]bIsdonQ87"Ln:Ou>oUI/Mq^bb\DmJ6,'CW1H$^d>[[CQogFddhjW4K;jdn@#"PA+0>>cl1_#F't5f\4e(k<olLdMSH!UV@BQFCV0&BF\K]u'e^LW*;\A#QYodjSlS'WG<m@=4JnFl"43]QEOBSi1U7.?;!Ru<)Us@!n?rFGe'R/!/Ggi_n!,00'7K~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 8
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000414 00000 n
|
||||||
|
0000000482 00000 n
|
||||||
|
0000000765 00000 n
|
||||||
|
0000000824 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<54fac52a7e33f1aa3f45091c2782a3eb><54fac52a7e33f1aa3f45091c2782a3eb>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 5 0 R
|
||||||
|
/Root 4 0 R
|
||||||
|
/Size 8
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2624
|
||||||
|
%%EOF
|
||||||
BIN
script.sql
Normal file
BIN
script.sql
Normal file
Binary file not shown.
453
search_pallets.py
Normal file
453
search_pallets.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
# search_pallets.py
|
||||||
|
# Finestra di ricerca su UDC / Lotto / Codice Prodotto
|
||||||
|
# - Tre campi: UDC, Lotto, Codice Prodotto (AND tra i campi valorizzati)
|
||||||
|
# - Ricerca su TUTTE le celle, incluse IDCella=9999 e corsia 7G
|
||||||
|
# - Risultati in una griglia: IDCella, Ubicazione, UDC, Lotto, Codice, Descrizione
|
||||||
|
# - Se la ricerca restituisce >0 righe, i campi input vengono svuotati
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
from gestione_aree_frame_async import BusyOverlay, AsyncRunner
|
||||||
|
from tkinter import filedialog
|
||||||
|
|
||||||
|
# opzionale export xlsx
|
||||||
|
try:
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment
|
||||||
|
_HAS_XLSX = True
|
||||||
|
except Exception:
|
||||||
|
_HAS_XLSX = False
|
||||||
|
|
||||||
|
# opzionale: usare tksheet per avere griglie con bordi di cella
|
||||||
|
try:
|
||||||
|
from tksheet import Sheet
|
||||||
|
except Exception:
|
||||||
|
Sheet = None
|
||||||
|
|
||||||
|
SQL_SEARCH = r"""
|
||||||
|
WITH BASE AS (
|
||||||
|
SELECT
|
||||||
|
g.IDCella,
|
||||||
|
-- forza stringa per ricerche LIKE
|
||||||
|
CONCAT(g.BarcodePallet, '') AS UDC,
|
||||||
|
c.Corsia,
|
||||||
|
c.Colonna,
|
||||||
|
c.Fila
|
||||||
|
FROM dbo.XMag_GiacenzaPallet AS g
|
||||||
|
LEFT JOIN dbo.Celle AS c ON c.ID = g.IDCella
|
||||||
|
-- NB: qui NON escludiamo IDCella=9999 né '7G'
|
||||||
|
),
|
||||||
|
JOINED AS (
|
||||||
|
SELECT
|
||||||
|
b.IDCella,
|
||||||
|
b.UDC,
|
||||||
|
b.Corsia,
|
||||||
|
b.Colonna,
|
||||||
|
b.Fila,
|
||||||
|
t.Lotto,
|
||||||
|
t.Prodotto,
|
||||||
|
t.Descrizione
|
||||||
|
FROM BASE b
|
||||||
|
LEFT JOIN dbo.vXTracciaProdotti AS t
|
||||||
|
ON t.Pallet COLLATE Latin1_General_CI_AS = LEFT(b.UDC, 6) COLLATE Latin1_General_CI_AS
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
j.IDCella,
|
||||||
|
UPPER(
|
||||||
|
CONCAT(
|
||||||
|
COALESCE(LTRIM(RTRIM(j.Corsia)), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(j.Colonna AS varchar(32)))), 'NA'), '.',
|
||||||
|
COALESCE(LTRIM(RTRIM(CAST(j.Fila AS varchar(32)))), 'NA')
|
||||||
|
)
|
||||||
|
) AS Ubicazione,
|
||||||
|
j.UDC,
|
||||||
|
j.Lotto,
|
||||||
|
j.Prodotto,
|
||||||
|
j.Descrizione
|
||||||
|
FROM JOINED j
|
||||||
|
WHERE 1=1
|
||||||
|
AND ( :udc IS NULL OR j.UDC COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :udc, '%') )
|
||||||
|
AND ( :lotto IS NULL OR j.Lotto COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :lotto, '%') )
|
||||||
|
AND ( :codice IS NULL OR j.Prodotto COLLATE Latin1_General_CI_AS LIKE CONCAT('%', :codice, '%') )
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN j.IDCella = 9999 THEN 1 ELSE 0 END,
|
||||||
|
j.Corsia, j.Colonna, j.Fila, j.UDC, j.Lotto, j.Prodotto;
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SearchWindow(ctk.CTkToplevel):
|
||||||
|
def __init__(self, parent: tk.Widget, db_app):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Warehouse · Ricerca UDC/Lotto/Codice")
|
||||||
|
self.geometry("1100x720")
|
||||||
|
self.minsize(900, 560)
|
||||||
|
self.resizable(True, True)
|
||||||
|
|
||||||
|
self.db = db_app
|
||||||
|
self._busy = BusyOverlay(self)
|
||||||
|
self._async = AsyncRunner(self)
|
||||||
|
|
||||||
|
# stato ordinamento colonne (col -> reverse bool)
|
||||||
|
self._sort_state: dict[str, bool] = {}
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
# layout griglia principale
|
||||||
|
self.grid_rowconfigure(0, weight=0)
|
||||||
|
self.grid_rowconfigure(1, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# --- barra di ricerca ---
|
||||||
|
top = ctk.CTkFrame(self)
|
||||||
|
top.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
|
||||||
|
for i in range(8):
|
||||||
|
top.grid_columnconfigure(i, weight=0)
|
||||||
|
top.grid_columnconfigure(7, weight=1)
|
||||||
|
|
||||||
|
ctk.CTkLabel(top, text="UDC:").grid(row=0, column=0, sticky="w")
|
||||||
|
self.var_udc = tk.StringVar()
|
||||||
|
e_udc = ctk.CTkEntry(top, textvariable=self.var_udc, width=160)
|
||||||
|
e_udc.grid(row=0, column=1, sticky="w", padx=(4, 12))
|
||||||
|
|
||||||
|
ctk.CTkLabel(top, text="Lotto:").grid(row=0, column=2, sticky="w")
|
||||||
|
self.var_lotto = tk.StringVar()
|
||||||
|
e_lotto = ctk.CTkEntry(top, textvariable=self.var_lotto, width=140)
|
||||||
|
e_lotto.grid(row=0, column=3, sticky="w", padx=(4, 12))
|
||||||
|
|
||||||
|
ctk.CTkLabel(top, text="Codice prodotto:").grid(row=0, column=4, sticky="w")
|
||||||
|
self.var_codice = tk.StringVar()
|
||||||
|
e_cod = ctk.CTkEntry(top, textvariable=self.var_codice, width=160)
|
||||||
|
e_cod.grid(row=0, column=5, sticky="w", padx=(4, 12))
|
||||||
|
|
||||||
|
btn = ctk.CTkButton(top, text="Cerca", command=self._do_search)
|
||||||
|
btn.grid(row=0, column=6, sticky="w")
|
||||||
|
|
||||||
|
btn_exp = ctk.CTkButton(top, text="Esporta XLSX", command=self._export_xlsx)
|
||||||
|
btn_exp.grid(row=0, column=7, sticky="e")
|
||||||
|
|
||||||
|
# --- griglia risultati ---
|
||||||
|
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)
|
||||||
|
|
||||||
|
# forza modalità Treeview (niente tksheet) per stabilità
|
||||||
|
self.use_sheet = False
|
||||||
|
|
||||||
|
cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione")
|
||||||
|
self.tree = ttk.Treeview(wrap, columns=cols, show="headings")
|
||||||
|
# stile: zebra + header leggibile
|
||||||
|
self._style = ttk.Style(self)
|
||||||
|
try:
|
||||||
|
self._style.theme_use(self._style.theme_use())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._style.configure("Search.Treeview", rowheight=22, font=("", 9))
|
||||||
|
self._style.configure("Search.Treeview.Heading", font=("", 9, "bold"), background="#F3F4F6")
|
||||||
|
self._style.map("Search.Treeview", background=[("selected", "#DCEBFF")])
|
||||||
|
self.tree.configure(style="Search.Treeview")
|
||||||
|
# tag per righe alternate + id9999 evidenziate
|
||||||
|
self.tree.tag_configure("even", background="#FFFFFF")
|
||||||
|
self.tree.tag_configure("odd", background="#F7F9FC")
|
||||||
|
# evidenzia spediti (IDCella=9999) in rosato tenue
|
||||||
|
self.tree.tag_configure("id9999", 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")
|
||||||
|
|
||||||
|
# doppio click → copia UDC; gestione click header
|
||||||
|
self.tree.bind("<Double-1>", self._on_dclick)
|
||||||
|
self.tree.bind("<Button-1>", self._maybe_handle_heading_click, add=True)
|
||||||
|
self.tree.bind("<Double-1>", self._on_heading_double_click, add=True)
|
||||||
|
|
||||||
|
def _apply_zebra(self):
|
||||||
|
for i, iid in enumerate(self.tree.get_children("")):
|
||||||
|
vals = self.tree.item(iid, "values")
|
||||||
|
zebra = "even" if i % 2 == 0 else "odd"
|
||||||
|
is9999 = False
|
||||||
|
if vals:
|
||||||
|
try:
|
||||||
|
is9999 = int(vals[0]) == 9999
|
||||||
|
except Exception:
|
||||||
|
is9999 = False
|
||||||
|
tags = ("id9999", zebra) if is9999 else (zebra,)
|
||||||
|
self.tree.item(iid, tags=tags)
|
||||||
|
|
||||||
|
# ---------------- AZIONI ----------------
|
||||||
|
def _export_xlsx(self):
|
||||||
|
# raccogli dati dalla griglia
|
||||||
|
rows = []
|
||||||
|
for iid in self.tree.get_children(""):
|
||||||
|
rows.append(self.tree.item(iid, "values"))
|
||||||
|
if not rows:
|
||||||
|
messagebox.showinfo("Esporta", "Non ci sono righe da esportare.", parent=self)
|
||||||
|
return
|
||||||
|
if not _HAS_XLSX:
|
||||||
|
messagebox.showerror("Esporta", "Per l'esportazione serve 'openpyxl' (pip install openpyxl).", parent=self)
|
||||||
|
return
|
||||||
|
# dialog salvataggio
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.now().strftime("%d_%m_%Y_%H-%M")
|
||||||
|
default_name = f"esportazione_ricerca_{ts}.xlsx"
|
||||||
|
fname = filedialog.asksaveasfilename(parent=self, title="Esporta in Excel",
|
||||||
|
defaultextension=".xlsx",
|
||||||
|
filetypes=[("Excel Workbook","*.xlsx")],
|
||||||
|
initialfile=default_name)
|
||||||
|
if not fname:
|
||||||
|
return
|
||||||
|
# crea workbook e scrivi
|
||||||
|
try:
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Risultati"
|
||||||
|
headers = ("IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione")
|
||||||
|
for j, h in enumerate(headers, start=1):
|
||||||
|
c = ws.cell(row=1, column=j, value=h)
|
||||||
|
c.font = Font(bold=True)
|
||||||
|
c.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
r = 2
|
||||||
|
for row in rows:
|
||||||
|
for j, v in enumerate(row, start=1):
|
||||||
|
ws.cell(row=r, column=j, value=v)
|
||||||
|
r += 1
|
||||||
|
# autosize
|
||||||
|
widths = {}
|
||||||
|
for row in ws.iter_rows(values_only=True):
|
||||||
|
for j, val in enumerate(row, start=1):
|
||||||
|
s = "" if val is None else str(val)
|
||||||
|
widths[j] = max(widths.get(j, 0), len(s))
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
for j, w in widths.items():
|
||||||
|
ws.column_dimensions[get_column_letter(j)].width = min(max(w + 2, 10), 60)
|
||||||
|
wb.save(fname)
|
||||||
|
messagebox.showinfo("Esporta", f"File creato:\n{fname}", parent=self)
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
messagebox.showerror("Esporta", f"Errore durante l'esportazione:{ex}", parent=self)
|
||||||
|
def _on_dclick(self, evt):
|
||||||
|
# copia UDC solo se il doppio click avviene su una cella, non sull'header
|
||||||
|
region = self.tree.identify("region", evt.x, evt.y)
|
||||||
|
if region != "cell":
|
||||||
|
return
|
||||||
|
sel = self.tree.focus()
|
||||||
|
if not sel:
|
||||||
|
return
|
||||||
|
vals = self.tree.item(sel, "values")
|
||||||
|
if len(vals) >= 3 and vals[2]: # UDC
|
||||||
|
try:
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(vals[2])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --------------- ORDINAMENTO COLONNE ---------------
|
||||||
|
def _maybe_handle_heading_click(self, evt):
|
||||||
|
# evita che il click sulle intestazioni selezioni una riga fantasma
|
||||||
|
region = self.tree.identify("region", evt.x, evt.y)
|
||||||
|
if region == "heading":
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _on_heading_double_click(self, evt):
|
||||||
|
# doppio click su intestazione: ordina la colonna corrispondente
|
||||||
|
region = self.tree.identify("region", evt.x, evt.y)
|
||||||
|
if region != "heading":
|
||||||
|
return
|
||||||
|
col_id = self.tree.identify_column(evt.x) # es. '#1'
|
||||||
|
try:
|
||||||
|
idx = int(col_id.replace('#','')) - 1
|
||||||
|
except Exception:
|
||||||
|
return "break"
|
||||||
|
cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione")
|
||||||
|
if 0 <= idx < len(cols):
|
||||||
|
self._sort_by_column(cols[idx])
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _sort_key_for_col(self, col: str, val: str):
|
||||||
|
if val is None:
|
||||||
|
return (1, "") # None in fondo
|
||||||
|
s = str(val)
|
||||||
|
if col in ("IDCella",):
|
||||||
|
# prova numero
|
||||||
|
try:
|
||||||
|
return (0, int(s))
|
||||||
|
except Exception:
|
||||||
|
return (0, s.lower())
|
||||||
|
if col in ("Lotto", "Codice", "UDC"):
|
||||||
|
return (0, s.lower())
|
||||||
|
return (0, s.lower())
|
||||||
|
|
||||||
|
def _sort_by_column(self, col: str):
|
||||||
|
try:
|
||||||
|
# toggle reverse
|
||||||
|
rev = self._sort_state.get(col, False)
|
||||||
|
self._sort_state[col] = not rev
|
||||||
|
|
||||||
|
# raccogli dati correnti
|
||||||
|
rows = []
|
||||||
|
for iid in self.tree.get_children(""):
|
||||||
|
vals = self.tree.item(iid, "values")
|
||||||
|
row = {"iid": iid,
|
||||||
|
"IDCella": vals[0] if len(vals) > 0 else None,
|
||||||
|
"Ubicazione": vals[1] if len(vals) > 1 else None,
|
||||||
|
"UDC": vals[2] if len(vals) > 2 else None,
|
||||||
|
"Lotto": vals[3] if len(vals) > 3 else None,
|
||||||
|
"Codice": vals[4] if len(vals) > 4 else None,
|
||||||
|
"Descrizione": vals[5] if len(vals) > 5 else None}
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
rows.sort(key=lambda r: self._sort_key_for_col(col, r.get(col)), reverse=rev)
|
||||||
|
|
||||||
|
def _apply_moves():
|
||||||
|
for index, r in enumerate(rows):
|
||||||
|
self.tree.move(r["iid"], "", index)
|
||||||
|
# aggiorna indicatori visuali nelle heading (▲/▼)
|
||||||
|
for k in ("IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione"):
|
||||||
|
base = {
|
||||||
|
"IDCella": "IDCella",
|
||||||
|
"Ubicazione": "Ubicazione",
|
||||||
|
"UDC": "UDC / Barcode",
|
||||||
|
"Lotto": "Lotto",
|
||||||
|
"Codice": "Codice prodotto",
|
||||||
|
"Descrizione": "Descrizione prodotto",
|
||||||
|
}[k]
|
||||||
|
if k == col:
|
||||||
|
arrow = " ▼" if not rev else " ▲"
|
||||||
|
self.tree.heading(k, text=base + arrow)
|
||||||
|
else:
|
||||||
|
self.tree.heading(k, text=base)
|
||||||
|
self._apply_zebra()
|
||||||
|
|
||||||
|
# posticipa le move per evitare re‑entrancy su doppio click
|
||||||
|
self.after_idle(_apply_moves)
|
||||||
|
except Exception:
|
||||||
|
# in caso di problemi non lasciamo la finestra in stato incoerente
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- ordinamento per tksheet (doppio click sui titoli) ---
|
||||||
|
def _on_sheet_header_double_click(self, event_dict):
|
||||||
|
try:
|
||||||
|
c = event_dict.get("column")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if c is None:
|
||||||
|
return
|
||||||
|
headers = ["IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione"]
|
||||||
|
if not (0 <= c < len(headers)):
|
||||||
|
return
|
||||||
|
colname = headers[c]
|
||||||
|
rev = self._sort_state.get(colname, False)
|
||||||
|
self._sort_state[colname] = not rev
|
||||||
|
|
||||||
|
data = self.sheet.get_sheet_data(return_copy=True)
|
||||||
|
def keyf(row):
|
||||||
|
val = row[c] if c < len(row) else None
|
||||||
|
if val is None:
|
||||||
|
return (1, "")
|
||||||
|
s = str(val)
|
||||||
|
if colname == "IDCella":
|
||||||
|
try:
|
||||||
|
return (0, int(s))
|
||||||
|
except Exception:
|
||||||
|
return (0, s.lower())
|
||||||
|
return (0, s.lower())
|
||||||
|
data.sort(key=keyf, reverse=rev)
|
||||||
|
self.sheet.set_sheet_data(data)
|
||||||
|
# evidenzia l'header ordinato (semplice: cambia testo temporaneamente)
|
||||||
|
try:
|
||||||
|
arrow = " ▼" if not rev else " ▲"
|
||||||
|
hdrs = list(headers)
|
||||||
|
hdrs[c] = hdrs[c] + arrow
|
||||||
|
self.sheet.headers(hdrs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _do_search(self):
|
||||||
|
udc = (self.var_udc.get() or "").strip()
|
||||||
|
lotto = (self.var_lotto.get() or "").strip()
|
||||||
|
codice = (self.var_codice.get() or "").strip()
|
||||||
|
|
||||||
|
# Se nessun filtro, chiedi conferma (evita estrazione enorme non voluta)
|
||||||
|
if not (udc or lotto or codice):
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"Conferma",
|
||||||
|
"Nessun filtro impostato. Vuoi cercare su TUTTO il magazzino?",
|
||||||
|
parent=self,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parametri: passa NULL se campo vuoto -> i filtri "si spengono"
|
||||||
|
params = {
|
||||||
|
"udc": (udc if udc else None),
|
||||||
|
"lotto": (lotto if lotto else None),
|
||||||
|
"codice": (codice if codice else None),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _ok(res):
|
||||||
|
rows = res.get("rows", []) if isinstance(res, dict) else []
|
||||||
|
# --- popola UI ---
|
||||||
|
if self.use_sheet:
|
||||||
|
try:
|
||||||
|
data = []
|
||||||
|
for r in rows:
|
||||||
|
idc, ubi, udc_v, lot_v, cod_v, desc_v = r
|
||||||
|
data.append([idc, ubi, udc_v, lot_v, cod_v, desc_v])
|
||||||
|
self.sheet.set_sheet_data(data)
|
||||||
|
self.sheet.set_all_cell_sizes_to_text()
|
||||||
|
except Exception as ex:
|
||||||
|
# fallback di sicurezza su Treeview
|
||||||
|
self.use_sheet = False
|
||||||
|
if not self.use_sheet:
|
||||||
|
# Treeview
|
||||||
|
for iid in self.tree.get_children():
|
||||||
|
self.tree.delete(iid)
|
||||||
|
for idx, r in enumerate(rows):
|
||||||
|
idc, ubi, udc_v, lot_v, cod_v, desc_v = r
|
||||||
|
zebra = "even" if idx % 2 == 0 else "odd"
|
||||||
|
try:
|
||||||
|
is9999 = int(idc) == 9999
|
||||||
|
except Exception:
|
||||||
|
is9999 = False
|
||||||
|
tags = ("id9999", zebra) if is9999 else (zebra,)
|
||||||
|
self.tree.insert("", "end", values=(idc, ubi, udc_v, lot_v, cod_v, desc_v), tags=tags)
|
||||||
|
|
||||||
|
# --- feedback utente ---
|
||||||
|
if not rows:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Nessun risultato",
|
||||||
|
"Nessuna corrispondenza trovata con le chiavi di ricerca inserite.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# reset campi se risultato non vuoto
|
||||||
|
self.var_udc.set("")
|
||||||
|
self.var_lotto.set("")
|
||||||
|
self.var_codice.set("")
|
||||||
|
self._busy.hide()
|
||||||
|
|
||||||
|
def _err(ex):
|
||||||
|
self._busy.hide()
|
||||||
|
messagebox.showerror("Errore ricerca", str(ex), parent=self)
|
||||||
|
|
||||||
|
self._async.run(self.db.query_json(SQL_SEARCH, params), _ok, _err, busy=self._busy, message="Cerco…")
|
||||||
|
|
||||||
|
|
||||||
|
def open_search_window(parent, db_app):
|
||||||
|
key = "_search_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 = SearchWindow(parent, db_app)
|
||||||
|
setattr(parent, key, w)
|
||||||
|
return w
|
||||||
21
sql.txt
Normal file
21
sql.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
SELECT * FROM vViewMappaturaDescrizioneCorsia WHERE ( Area > 0) ORDER BY Area Desc
|
||||||
|
|
||||||
|
SELECT * FROM Celle WHERE ( ID > 0) ORDER BY ID Desc
|
||||||
|
|
||||||
|
SELECT TOP 1000 [mc_X] ,[minFila] ,[maxFila] ,[minColonna] ,[maxColonna] ,[mc_Numero_Magazzino] ,[mc_Numero_Area] FROM vViewMappaturaPosizCorsia WHERE [mc_Numero_Area] =1
|
||||||
|
|
||||||
|
|
||||||
|
SELECT CASE WHEN Nota = 'ASC' THEN 0 ELSE CASE WHEN Nota = 'DESC' THEN 1 END END AS Nota FROM MagLayout WHERE (IDArea = 1)
|
||||||
|
|
||||||
|
SELECT ID, Descrizione FROM Magazzini ORDER BY Descrizione
|
||||||
|
|
||||||
|
SELECT ID, Descrizione FROM Celle ORDER BY Descrizione
|
||||||
|
|
||||||
|
SELECT [ID],[CorsiaDescrizione] FROM vViewMappaturaDescrizioneCorsia
|
||||||
|
|
||||||
|
|
||||||
|
SELECT COUNT(DISTINCT Pallet) AS Pallet, COUNT(DISTINCT Lotto) AS Lotto, COUNT(DISTINCT Articolo) AS Articolo, COUNT(DISTINCT Descrizione) AS Descrizione, SUM(Qta) AS Qta, Documento, CodNazione, NAZIONE, Stato, MAX(PalletCella) AS PalletCella, MAX(Magazzino) AS Magazzino, MAX(Area) AS Area, MAX(Cella) AS Cella, MIN(Ordinamento) AS Ordinamento, MAX(IDStato) AS IDStato FROM dbo.XMag_ViewPackingList GROUP BY Documento, CodNazione, NAZIONE, Stato
|
||||||
|
|
||||||
|
SELECT * FROM vViewPackingListRestante WHERE Documento = 237 ORDER BY Ordinamento
|
||||||
|
SELECT * FROM vViewPackingListRestante WHERE Documento = 240 ORDER BY Ordinamento
|
||||||
|
|
||||||
346
view_celle_multiple.py
Normal file
346
view_celle_multiple.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
# view_celle_multiple.py
|
||||||
|
import json
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, filedialog
|
||||||
|
import customtkinter as ctk
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment
|
||||||
|
|
||||||
|
from gestione_aree_frame_async import AsyncRunner
|
||||||
|
|
||||||
|
def _json_obj(res):
|
||||||
|
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):
|
||||||
|
def __init__(self, root, db_client, runner: AsyncRunner | None = None):
|
||||||
|
super().__init__(root)
|
||||||
|
self.title("Celle con più 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):
|
||||||
|
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)
|
||||||
|
|
||||||
|
f = ctk.CTkFrame(self); f.grid(row=1, column=0, sticky="nsew", padx=6, pady=(0,6))
|
||||||
|
f.grid_rowconfigure(0, weight=1); f.grid_columnconfigure(0, weight=1)
|
||||||
|
self.tree = ttk.Treeview(f, 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(f, orient="vertical", command=self.tree.yview)
|
||||||
|
x = ttk.Scrollbar(f, 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 k,t,w,a in (("Corsia","Corsia",100,"center"),
|
||||||
|
("TotCelle","Totale celle",120,"e"),
|
||||||
|
("CelleMultiple",">1 UDC",120,"e"),
|
||||||
|
("Percentuale","%",80,"e")):
|
||||||
|
self.sum_tbl.heading(k, text=t); self.sum_tbl.column(k, width=w, anchor=a)
|
||||||
|
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):
|
||||||
|
self.tree.bind("<<TreeviewOpen>>", self._on_open_node)
|
||||||
|
|
||||||
|
def refresh_all(self):
|
||||||
|
self._load_corsie(); self._load_riepilogo()
|
||||||
|
|
||||||
|
def _load_corsie(self):
|
||||||
|
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):
|
||||||
|
rows = _json_obj(res).get("rows", [])
|
||||||
|
for r in rows:
|
||||||
|
corsia = r.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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
rows = _json_obj(res).get("rows", [])
|
||||||
|
if not rows:
|
||||||
|
self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "")); return
|
||||||
|
for r in rows:
|
||||||
|
idc = r["IDCella"]; ubi = r["Ubicazione"]; corsia = r.get("Corsia"); num = r.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(ch.endswith("::lazy") for ch 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):
|
||||||
|
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):
|
||||||
|
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((t for t in parent_tags if t.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 r in rows:
|
||||||
|
pallet = r.get("Pallet", ""); desc = r.get("Descrizione", ""); lotto = r.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):
|
||||||
|
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):
|
||||||
|
rows = _json_obj(res).get("rows", [])
|
||||||
|
for i in self.sum_tbl.get_children(): self.sum_tbl.delete(i)
|
||||||
|
for r in rows:
|
||||||
|
self.sum_tbl.insert("", "end", values=(r.get("Corsia"), r.get("TotCelle",0),
|
||||||
|
r.get("CelleMultiple",0), f"{r.get('Percentuale',0):.2f}"))
|
||||||
|
|
||||||
|
def expand_all(self):
|
||||||
|
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):
|
||||||
|
for iid in self.tree.get_children(""):
|
||||||
|
self.tree.item(iid, open=False)
|
||||||
|
|
||||||
|
def export_to_xlsx(self):
|
||||||
|
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):
|
||||||
|
for j,h in enumerate(headers, start=1):
|
||||||
|
cell = ws.cell(row=1, column=j, value=h)
|
||||||
|
cell.font = Font(bold=True); cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
_hdr(ws_det, det_headers); _hdr(ws_sum, sum_headers)
|
||||||
|
|
||||||
|
r = 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((t.split(":",1)[1] for t in tags if t.startswith("corsia:")), "")
|
||||||
|
ubi = next((t.split(":",1)[1] for t in tags if t.startswith("ubicazione:")), "")
|
||||||
|
idcella = next((t.split(":",1)[1] for t in tags if t.startswith("idcella:")), "")
|
||||||
|
pallet = self.tree.item(pallet_node, "text")
|
||||||
|
desc, lotto = self.tree.item(pallet_node, "values")
|
||||||
|
for j,v in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1):
|
||||||
|
ws_det.cell(row=r, column=j, value=v)
|
||||||
|
r += 1
|
||||||
|
|
||||||
|
r2 = 2
|
||||||
|
for iid in self.sum_tbl.get_children(""):
|
||||||
|
vals = self.sum_tbl.item(iid, "values")
|
||||||
|
for j, v in enumerate(vals, start=1):
|
||||||
|
ws_sum.cell(row=r2, column=j, value=v)
|
||||||
|
r2 += 1
|
||||||
|
|
||||||
|
def _autosize(ws):
|
||||||
|
widths = {}
|
||||||
|
for row in ws.iter_rows(values_only=True):
|
||||||
|
for j, val in enumerate(row, start=1):
|
||||||
|
val_s = "" if val is None else str(val)
|
||||||
|
widths[j] = max(widths.get(j, 0), len(val_s))
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
for j, w in widths.items():
|
||||||
|
ws.column_dimensions[get_column_letter(j)].width = min(max(w + 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):
|
||||||
|
win = CelleMultipleWindow(root, db_client, runner=runner); win.lift(); win.focus_set(); return win
|
||||||
1712
warehouse_sp_python.py
Normal file
1712
warehouse_sp_python.py
Normal file
File diff suppressed because it is too large
Load Diff
3270
warehouse_sql.log
Normal file
3270
warehouse_sql.log
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user