diff --git a/__pycache__/async_loop_singleton.cpython-313.pyc b/__pycache__/async_loop_singleton.cpython-313.pyc index c366516..e6e0862 100644 Binary files a/__pycache__/async_loop_singleton.cpython-313.pyc and b/__pycache__/async_loop_singleton.cpython-313.pyc differ diff --git a/__pycache__/async_msssql_query.cpython-313.pyc b/__pycache__/async_msssql_query.cpython-313.pyc index 1c97cb0..fa34eb0 100644 Binary files a/__pycache__/async_msssql_query.cpython-313.pyc and b/__pycache__/async_msssql_query.cpython-313.pyc differ diff --git a/__pycache__/async_runner.cpython-313.pyc b/__pycache__/async_runner.cpython-313.pyc deleted file mode 100644 index 55c3ac3..0000000 Binary files a/__pycache__/async_runner.cpython-313.pyc and /dev/null differ diff --git a/__pycache__/db_async_singleton.cpython-313.pyc b/__pycache__/db_async_singleton.cpython-313.pyc deleted file mode 100644 index 47583db..0000000 Binary files a/__pycache__/db_async_singleton.cpython-313.pyc and /dev/null differ diff --git a/__pycache__/gestione_aree_frame_async.cpython-313.pyc b/__pycache__/gestione_aree_frame_async.cpython-313.pyc index f7f088f..f696bcc 100644 Binary files a/__pycache__/gestione_aree_frame_async.cpython-313.pyc and b/__pycache__/gestione_aree_frame_async.cpython-313.pyc differ diff --git a/__pycache__/gestione_pickinglist.cpython-313.pyc b/__pycache__/gestione_pickinglist.cpython-313.pyc index ef992ed..c9668ab 100644 Binary files a/__pycache__/gestione_pickinglist.cpython-313.pyc and b/__pycache__/gestione_pickinglist.cpython-313.pyc differ diff --git a/__pycache__/layout_window.cpython-313.pyc b/__pycache__/layout_window.cpython-313.pyc index 2a5659d..99221d1 100644 Binary files a/__pycache__/layout_window.cpython-313.pyc and b/__pycache__/layout_window.cpython-313.pyc differ diff --git a/__pycache__/prenota_sprenota_sql.cpython-313.pyc b/__pycache__/prenota_sprenota_sql.cpython-313.pyc index 95518bc..65d5593 100644 Binary files a/__pycache__/prenota_sprenota_sql.cpython-313.pyc and b/__pycache__/prenota_sprenota_sql.cpython-313.pyc differ diff --git a/__pycache__/reset_corsie.cpython-313.pyc b/__pycache__/reset_corsie.cpython-313.pyc index 0be68af..951750c 100644 Binary files a/__pycache__/reset_corsie.cpython-313.pyc and b/__pycache__/reset_corsie.cpython-313.pyc differ diff --git a/__pycache__/search_pallets.cpython-313.pyc b/__pycache__/search_pallets.cpython-313.pyc index 775cdc1..3d97e84 100644 Binary files a/__pycache__/search_pallets.cpython-313.pyc and b/__pycache__/search_pallets.cpython-313.pyc differ diff --git a/__pycache__/view_celle_multiple.cpython-313.pyc b/__pycache__/view_celle_multiple.cpython-313.pyc index 446d29f..58ab1f2 100644 Binary files a/__pycache__/view_celle_multiple.cpython-313.pyc and b/__pycache__/view_celle_multiple.cpython-313.pyc differ diff --git a/async_loop_singleton.py b/async_loop_singleton.py index ad765e3..2a218cc 100644 --- a/async_loop_singleton.py +++ b/async_loop_singleton.py @@ -1,16 +1,31 @@ -# async_loop_singleton.py -import asyncio, threading -from typing import Callable +"""Shared asyncio loop lifecycle helpers for the desktop application. + +The GUI runs on Tk's main thread, while database calls are executed on a +dedicated background event loop. These helpers expose that loop as a lazy +singleton so every module can schedule work on the same async runtime. +""" + +import asyncio +import threading + class _LoopHolder: + """Store the global loop instance and its worker thread.""" + 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.""" + """Return the shared background event loop. + + The loop is created lazily the first time the function is called and kept + alive for the lifetime of the application. + """ if _GLOBAL.loop: return _GLOBAL.loop @@ -27,7 +42,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop: ready.wait() return _GLOBAL.loop + def stop_global_loop(): + """Stop the shared loop and join the background thread if present.""" if _GLOBAL.loop and _GLOBAL.loop.is_running(): _GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop) _GLOBAL.thread.join(timeout=2) diff --git a/async_msssql_query.py b/async_msssql_query.py index be00967..843cf0a 100644 --- a/async_msssql_query.py +++ b/async_msssql_query.py @@ -1,69 +1,110 @@ -# async_msssql_query.py — loop-safe, compat rows=list, no pooling +"""Async SQL Server access layer used by the warehouse application. + +The module centralizes DSN creation and exposes :class:`AsyncMSSQLClient`, +which lazily binds a SQLAlchemy async engine to the running event loop. The +implementation intentionally avoids pooling because the GUI schedules work on a +single shared background loop and pooled connections were a source of +cross-loop errors. +""" + from __future__ import annotations -import asyncio, urllib.parse, time, logging +import asyncio +import logging +import time +import urllib.parse from typing import Any, Dict, Optional + +from sqlalchemy import text 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") + + def _dumps(obj: Any) -> str: + """Serialize an object to JSON using the fastest available backend.""" + 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 _dumps(obj: Any) -> str: + """Serialize an object to JSON using the standard library fallback.""" + 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 + *, + 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()) + ";" + """Build a SQLAlchemy ``mssql+aioodbc`` DSN from SQL Server parameters.""" + 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: + """Thin async query client for SQL Server. + + The engine is created lazily on the currently running event loop and uses + :class:`sqlalchemy.pool.NullPool` to avoid recycling connections across + loops or threads. """ - 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): + + def __init__(self, dsn: str, *, echo: bool = False, log: bool = True): + """Initialize the client without opening any connection immediately.""" 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) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s")) + self._logger.addHandler(handler) self._enable_log = log async def _ensure_engine(self): + """Create the async engine on first use for the current running loop.""" 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 + # NullPool avoids reusing connections bound to a different event loop. + poolclass=NullPool, + # aioodbc must explicitly receive the loop to bind to. + connect_args={"loop": loop}, ) self._engine_loop = loop if self._enable_log: self._logger.info("Engine created on loop %s", id(loop)) async def dispose(self): + """Dispose the engine on the loop where it was created.""" 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: @@ -73,7 +114,25 @@ class AsyncMSSQLClient: 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]: + async def query_json( + self, + sql: str, + params: Optional[Dict[str, Any]] = None, + *, + as_dict_rows: bool = False, + ) -> Dict[str, Any]: + """Execute a query and return a JSON-friendly payload. + + Args: + sql: SQL statement to execute. + params: Optional named parameters bound to the statement. + as_dict_rows: When ``True`` returns rows as dictionaries keyed by + column name; otherwise rows are returned as lists. + + Returns: + A dictionary containing column names, rows and elapsed execution + time in milliseconds. + """ await self._ensure_engine() t0 = time.perf_counter() async with self._engine.connect() as conn: @@ -81,12 +140,17 @@ class AsyncMSSQLClient: rows = res.fetchall() cols = list(res.keys()) if as_dict_rows: - rows_out = [dict(zip(cols, r)) for r in rows] + rows_out = [dict(zip(cols, row)) for row 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)} + rows_out = [list(row) for row 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: + async def exec(self, sql: str, params: Optional[Dict[str, Any]] = None, *, commit: bool = False) -> int: + """Execute a DML statement and return its row count.""" 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 {}) diff --git a/db_async_singleton.py b/db_async_singleton.py deleted file mode 100644 index 535df6b..0000000 --- a/db_async_singleton.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..cec44c4 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,11 @@ +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: html clean + +html: + $(SPHINXBUILD) -b html $(SOURCEDIR) $(BUILDDIR)/html + +clean: + $(SPHINXBUILD) -M clean $(SOURCEDIR) $(BUILDDIR) diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo new file mode 100644 index 0000000..2a685e7 --- /dev/null +++ b/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 5114b3e0495dd40f0c4652b28de89aa4 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/.doctrees/api_reference.doctree b/docs/_build/html/.doctrees/api_reference.doctree new file mode 100644 index 0000000..a05ec0f Binary files /dev/null and b/docs/_build/html/.doctrees/api_reference.doctree differ diff --git a/docs/_build/html/.doctrees/architecture.doctree b/docs/_build/html/.doctrees/architecture.doctree new file mode 100644 index 0000000..8f67eb8 Binary files /dev/null and b/docs/_build/html/.doctrees/architecture.doctree differ diff --git a/docs/_build/html/.doctrees/environment.pickle b/docs/_build/html/.doctrees/environment.pickle new file mode 100644 index 0000000..aa4ea93 Binary files /dev/null and b/docs/_build/html/.doctrees/environment.pickle differ diff --git a/docs/_build/html/.doctrees/flows/README.doctree b/docs/_build/html/.doctrees/flows/README.doctree new file mode 100644 index 0000000..27eb3ac Binary files /dev/null and b/docs/_build/html/.doctrees/flows/README.doctree differ diff --git a/docs/_build/html/.doctrees/flows/async_db_flow.doctree b/docs/_build/html/.doctrees/flows/async_db_flow.doctree new file mode 100644 index 0000000..cb1b3cd Binary files /dev/null and b/docs/_build/html/.doctrees/flows/async_db_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/async_loop_singleton_flow.doctree b/docs/_build/html/.doctrees/flows/async_loop_singleton_flow.doctree new file mode 100644 index 0000000..b036dcf Binary files /dev/null and b/docs/_build/html/.doctrees/flows/async_loop_singleton_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/async_msssql_query_flow.doctree b/docs/_build/html/.doctrees/flows/async_msssql_query_flow.doctree new file mode 100644 index 0000000..14d9ba2 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/async_msssql_query_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/gestione_aree_frame_async_flow.doctree b/docs/_build/html/.doctrees/flows/gestione_aree_frame_async_flow.doctree new file mode 100644 index 0000000..2abd68d Binary files /dev/null and b/docs/_build/html/.doctrees/flows/gestione_aree_frame_async_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/gestione_pickinglist_flow.doctree b/docs/_build/html/.doctrees/flows/gestione_pickinglist_flow.doctree new file mode 100644 index 0000000..773b0d5 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/gestione_pickinglist_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/index.doctree b/docs/_build/html/.doctrees/flows/index.doctree new file mode 100644 index 0000000..d143cfe Binary files /dev/null and b/docs/_build/html/.doctrees/flows/index.doctree differ diff --git a/docs/_build/html/.doctrees/flows/layout_window_flow.doctree b/docs/_build/html/.doctrees/flows/layout_window_flow.doctree new file mode 100644 index 0000000..edc5df5 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/layout_window_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/main_flow.doctree b/docs/_build/html/.doctrees/flows/main_flow.doctree new file mode 100644 index 0000000..0b18ef2 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/main_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/reset_corsie_flow.doctree b/docs/_build/html/.doctrees/flows/reset_corsie_flow.doctree new file mode 100644 index 0000000..81048d6 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/reset_corsie_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/search_pallets_flow.doctree b/docs/_build/html/.doctrees/flows/search_pallets_flow.doctree new file mode 100644 index 0000000..d58a78c Binary files /dev/null and b/docs/_build/html/.doctrees/flows/search_pallets_flow.doctree differ diff --git a/docs/_build/html/.doctrees/flows/view_celle_multiple_flow.doctree b/docs/_build/html/.doctrees/flows/view_celle_multiple_flow.doctree new file mode 100644 index 0000000..ddf6c60 Binary files /dev/null and b/docs/_build/html/.doctrees/flows/view_celle_multiple_flow.doctree differ diff --git a/docs/_build/html/.doctrees/index.doctree b/docs/_build/html/.doctrees/index.doctree new file mode 100644 index 0000000..d7a3f3a Binary files /dev/null and b/docs/_build/html/.doctrees/index.doctree differ diff --git a/docs/_build/html/_sources/api_reference.rst.txt b/docs/_build/html/_sources/api_reference.rst.txt new file mode 100644 index 0000000..d790b83 --- /dev/null +++ b/docs/_build/html/_sources/api_reference.rst.txt @@ -0,0 +1,77 @@ +Riferimento API +=============== + +La sezione seguente usa ``autodoc`` per estrarre docstring direttamente dai +moduli Python principali del progetto. + +main.py +------- + +.. automodule:: main + :members: + :undoc-members: + :show-inheritance: + +async_msssql_query.py +--------------------- + +.. automodule:: async_msssql_query + :members: + :undoc-members: + :show-inheritance: + +gestione_aree_frame_async.py +---------------------------- + +.. automodule:: gestione_aree_frame_async + :members: + :undoc-members: + :show-inheritance: + +layout_window.py +---------------- + +.. automodule:: layout_window + :members: + :undoc-members: + :show-inheritance: + +reset_corsie.py +--------------- + +.. automodule:: reset_corsie + :members: + :undoc-members: + :show-inheritance: + +view_celle_multiple.py +---------------------- + +.. automodule:: view_celle_multiple + :members: + :undoc-members: + :show-inheritance: + +search_pallets.py +----------------- + +.. automodule:: search_pallets + :members: + :undoc-members: + :show-inheritance: + +gestione_pickinglist.py +----------------------- + +.. automodule:: gestione_pickinglist + :members: + :undoc-members: + :show-inheritance: + +prenota_sprenota_sql.py +----------------------- + +.. automodule:: prenota_sprenota_sql + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/architecture.md.txt b/docs/_build/html/_sources/architecture.md.txt new file mode 100644 index 0000000..4681e09 --- /dev/null +++ b/docs/_build/html/_sources/architecture.md.txt @@ -0,0 +1,51 @@ +# Architettura Complessiva + +Questa pagina collega i moduli principali del progetto in una vista unica, +partendo dal launcher fino ai moduli GUI e al livello infrastrutturale async/DB. + +## Vista architetturale + +```{mermaid} +flowchart TD + Main["main.py"] --> Launcher["Launcher"] + Main --> Loop["async_loop_singleton.get_global_loop()"] + Main --> DB["AsyncMSSQLClient"] + + Launcher --> Reset["reset_corsie.py"] + Launcher --> Layout["layout_window.py"] + Launcher --> Ghost["view_celle_multiple.py"] + Launcher --> Search["search_pallets.py"] + Launcher --> Picking["gestione_pickinglist.py"] + + Reset --> Runner["gestione_aree_frame_async.AsyncRunner"] + Layout --> Runner + Ghost --> Runner + Search --> Runner + Picking --> Runner + + Runner --> Loop + Runner --> DB + Picking --> SP["prenota_sprenota_sql.py"] + SP --> DB + DB --> SQL["SQL Server / Mediseawall"] +``` + +## Flusso applicativo generale + +```{mermaid} +flowchart LR + User["Utente"] --> MainWin["Launcher"] + MainWin --> Module["Finestra modulo"] + Module --> AsyncReq["AsyncRunner.run(...)"] + AsyncReq --> DbClient["AsyncMSSQLClient"] + DbClient --> SqlServer["Database SQL Server"] + SqlServer --> Callback["Callback _ok/_err"] + Callback --> Module +``` + +## Osservazioni + +- `main.py` centralizza il loop asincrono e il client database condiviso. +- I moduli GUI si concentrano sulla UI e delegano query e task lunghi a `AsyncRunner`. +- `gestione_pickinglist.py` è l'unico modulo che passa anche da `prenota_sprenota_sql.py` per la logica di prenotazione. +- La cartella `docs/flows/` contiene la vista dettagliata modulo per modulo. diff --git a/docs/_build/html/_sources/flows/README.md.txt b/docs/_build/html/_sources/flows/README.md.txt new file mode 100644 index 0000000..8b1c017 --- /dev/null +++ b/docs/_build/html/_sources/flows/README.md.txt @@ -0,0 +1,27 @@ +# Flow Diagrams + +Questa cartella contiene schemi di flusso e schemi di chiamata dei moduli +principali avviati da `main.py`. + +I diagrammi sono scritti in Mermaid, quindi possono essere: + +- letti direttamente nei file Markdown; +- renderizzati da molti editor Git/Markdown; +- inclusi in una futura documentazione Sphinx o MkDocs. + +## Indice + +- [main](./main_flow.md) +- [layout_window](./layout_window_flow.md) +- [reset_corsie](./reset_corsie_flow.md) +- [view_celle_multiple](./view_celle_multiple_flow.md) +- [search_pallets](./search_pallets_flow.md) +- [gestione_pickinglist](./gestione_pickinglist_flow.md) +- [infrastruttura async/db](./async_db_flow.md) + +## Convenzioni + +- I diagrammi descrivono il flusso applicativo ad alto livello. +- Non rappresentano ogni singola riga di codice. +- I nodi `AsyncRunner` e `query_json` evidenziano i passaggi asincroni più + importanti tra interfaccia e database. diff --git a/docs/_build/html/_sources/flows/async_db_flow.md.txt b/docs/_build/html/_sources/flows/async_db_flow.md.txt new file mode 100644 index 0000000..f0de6ed --- /dev/null +++ b/docs/_build/html/_sources/flows/async_db_flow.md.txt @@ -0,0 +1,39 @@ +# Infrastruttura Async / DB + +## Scopo + +Questo diagramma descrive il flusso comune usato da tutti i moduli GUI quando +eseguono una query sul database. + +## Flusso trasversale + +```{mermaid} +flowchart TD + A["Evento UI (click / selezione / ricerca)"] --> B["Metodo finestra"] + B --> C["AsyncRunner.run(awaitable)"] + C --> D["Coroutines sul loop globale"] + D --> E["AsyncMSSQLClient.query_json() / exec()"] + E --> F["SQL Server"] + F --> G["Risultato query"] + G --> H["Future completata"] + H --> I["Callback _ok / _err su thread Tk"] + I --> J["Aggiornamento widget"] +``` + +## Relazioni principali + +```{mermaid} +flowchart LR + Main["main.py"] --> Loop["get_global_loop()"] + Main --> DB["AsyncMSSQLClient"] + Windows["Moduli GUI"] --> Runner["AsyncRunner"] + Runner --> Loop + Runner --> DB + DB --> SQL["SQL Server Mediseawall"] +``` + +## Note + +- Il loop asincrono è condiviso tra tutte le finestre. +- Il client DB è condiviso e creato una sola volta nel launcher. +- I callback che aggiornano la UI rientrano sempre sul thread Tk. diff --git a/docs/_build/html/_sources/flows/async_loop_singleton_flow.md.txt b/docs/_build/html/_sources/flows/async_loop_singleton_flow.md.txt new file mode 100644 index 0000000..395dfbb --- /dev/null +++ b/docs/_build/html/_sources/flows/async_loop_singleton_flow.md.txt @@ -0,0 +1,39 @@ +# `async_loop_singleton.py` + +## Scopo + +Questo modulo mantiene un loop asyncio globale e condiviso, eseguito su un +thread dedicato. + +## Flusso + +```{mermaid} +flowchart TD + A["Chiamata a get_global_loop()"] --> B{"Loop gia presente?"} + B -- Si --> C["Ritorna loop esistente"] + B -- No --> D["Crea Event ready"] + D --> E["Avvia thread daemon"] + E --> F["_run()"] + F --> G["new_event_loop()"] + G --> H["set_event_loop(loop)"] + H --> I["ready.set()"] + I --> J["loop.run_forever()"] + J --> K["Ritorna loop al chiamante"] +``` + +## Chiusura + +```{mermaid} +flowchart TD + A["stop_global_loop()"] --> B{"Loop attivo?"} + B -- No --> C["Nessuna azione"] + B -- Si --> D["call_soon_threadsafe(loop.stop)"] + D --> E["join del thread"] + E --> F["Azzera riferimenti globali"] +``` + +## Note + +- E un helper minimale usato da `main.py`. +- Il modulo esiste separato da `gestione_aree_frame_async.py`, ma concettualmente + svolge lo stesso ruolo di gestione del loop condiviso. diff --git a/docs/_build/html/_sources/flows/async_msssql_query_flow.md.txt b/docs/_build/html/_sources/flows/async_msssql_query_flow.md.txt new file mode 100644 index 0000000..b53a283 --- /dev/null +++ b/docs/_build/html/_sources/flows/async_msssql_query_flow.md.txt @@ -0,0 +1,41 @@ +# `async_msssql_query.py` + +## Scopo + +Questo modulo centralizza la costruzione del DSN SQL Server e l'accesso +asincrono al database tramite `AsyncMSSQLClient`. + +## Flusso di utilizzo + +```{mermaid} +flowchart TD + A["main.py o modulo chiamante"] --> B["make_mssql_dsn(...)"] + B --> C["Crea stringa mssql+aioodbc"] + C --> D["AsyncMSSQLClient(dsn)"] + D --> E["query_json(...) o exec(...)"] + E --> F["_ensure_engine()"] + F --> G{"Engine gia creato?"} + G -- No --> H["create_async_engine(..., NullPool, loop corrente)"] + G -- Si --> I["Riusa engine esistente"] + H --> J["execute(text(sql), params)"] + I --> J + J --> K["Normalizza rows/columns"] + K --> L["Ritorna payload JSON-friendly"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + DSN["make_mssql_dsn"] --> Client["AsyncMSSQLClient.__init__"] + Client --> Ensure["_ensure_engine"] + Ensure --> Query["query_json"] + Ensure --> Exec["exec"] + Client --> Dispose["dispose"] +``` + +## Note + +- `NullPool` evita problemi di riuso connessioni tra loop diversi. +- L'engine viene creato solo al primo utilizzo reale. +- `query_json()` restituisce un formato gia pronto per le callback GUI. diff --git a/docs/_build/html/_sources/flows/gestione_aree_frame_async_flow.md.txt b/docs/_build/html/_sources/flows/gestione_aree_frame_async_flow.md.txt new file mode 100644 index 0000000..cb10953 --- /dev/null +++ b/docs/_build/html/_sources/flows/gestione_aree_frame_async_flow.md.txt @@ -0,0 +1,45 @@ +# `gestione_aree_frame_async.py` + +## Scopo + +Questo modulo fornisce l'infrastruttura async usata dalle finestre GUI: + +- loop asincrono globale; +- overlay di attesa; +- runner che collega coroutine e callback Tk. + +## Flusso infrastrutturale + +```{mermaid} +flowchart TD + A["Metodo finestra GUI"] --> B["AsyncRunner.run(awaitable)"] + B --> C{"busy overlay richiesto?"} + C -- Si --> D["BusyOverlay.show()"] + C -- No --> E["Salta overlay"] + D --> F["run_coroutine_threadsafe(awaitable, loop globale)"] + E --> F + F --> G["Polling del Future"] + G --> H{"Future completato?"} + H -- No --> G + H -- Si --> I{"Successo o errore?"} + I -- Successo --> J["widget.after(..., on_success)"] + I -- Errore --> K["widget.after(..., on_error)"] + J --> L["BusyOverlay.hide()"] + K --> L +``` + +## Schema di componenti + +```{mermaid} +flowchart LR + Holder["_LoopHolder"] --> Loop["get_global_loop"] + Loop --> Runner["AsyncRunner"] + Overlay["BusyOverlay"] --> Runner + Runner --> GUI["Moduli GUI"] +``` + +## Note + +- Il modulo fa da ponte tra thread Tk e thread del loop asincrono. +- `BusyOverlay` e riusato da piu finestre, quindi e un componente condiviso. +- `AsyncRunner` evita che i moduli GUI gestiscano direttamente i `Future`. diff --git a/docs/_build/html/_sources/flows/gestione_pickinglist_flow.md.txt b/docs/_build/html/_sources/flows/gestione_pickinglist_flow.md.txt new file mode 100644 index 0000000..e5aca94 --- /dev/null +++ b/docs/_build/html/_sources/flows/gestione_pickinglist_flow.md.txt @@ -0,0 +1,69 @@ +# `gestione_pickinglist.py` + +## Scopo + +Questo modulo gestisce la vista master/detail delle picking list e permette di: + +- caricare l'elenco dei documenti; +- vedere il dettaglio UDC della riga selezionata; +- prenotare e s-prenotare una picking list; +- mantenere una UI fluida con spinner e refresh differiti. + +## Flusso di apertura + +```{mermaid} +flowchart TD + A["open_pickinglist_window() da main.py"] --> B["create_pickinglist_frame()"] + B --> C["GestionePickingListFrame.__init__()"] + C --> D["_build_layout()"] + D --> E["after_idle(_first_show)"] + E --> F["reload_from_db(first=True)"] + F --> G["query_json SQL_PL"] + G --> H["_refresh_mid_rows()"] + H --> I["Render tabella master"] +``` + +## Flusso master/detail + +```{mermaid} +flowchart TD + A["Utente seleziona checkbox riga"] --> B["on_row_checked()"] + B --> C["Deseleziona altre righe"] + C --> D["Salva detail_doc"] + D --> E["query_json SQL_PL_DETAILS"] + E --> F["_refresh_details()"] + F --> G["Render tabella dettaglio"] +``` + +## Prenotazione / s-prenotazione + +```{mermaid} +flowchart TD + A["Click Prenota o S-prenota"] --> B["Verifica riga selezionata"] + B --> C["Determina documento e stato atteso"] + C --> D["Chiama sp_xExePackingListPallet_async()"] + D --> E["Aggiorna Celle e LogPackingList sul DB"] + E --> F["SPResult"] + F --> G{"rc == 0?"} + G -- Si --> H["_recolor_row_by_documento()"] + G -- No --> I["Messaggio di errore"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Init["__init__"] --> Build["_build_layout"] + Init --> First["_first_show"] + First --> Reload["reload_from_db"] + Reload --> Mid["_refresh_mid_rows"] + Check["on_row_checked"] --> Details["_refresh_details"] + Pren["on_prenota"] --> SP["sp_xExePackingListPallet_async"] + Spren["on_sprenota"] --> SP +``` + +## Note + +- Il modulo usa `AsyncRunner`, `BusyOverlay` e `ToolbarSpinner`. +- Il caricamento iniziale è differito con `after_idle` per ridurre lo sfarfallio. +- La riga selezionata viene tenuta separata dal dettaglio tramite `detail_doc`. diff --git a/docs/_build/html/_sources/flows/index.rst.txt b/docs/_build/html/_sources/flows/index.rst.txt new file mode 100644 index 0000000..9c2f9b0 --- /dev/null +++ b/docs/_build/html/_sources/flows/index.rst.txt @@ -0,0 +1,20 @@ +Flow Diagrams +============= + +Questa sezione raccoglie i diagrammi Mermaid dei moduli applicativi e +infrastrutturali. + +.. toctree:: + :maxdepth: 1 + + README.md + main_flow.md + layout_window_flow.md + reset_corsie_flow.md + view_celle_multiple_flow.md + search_pallets_flow.md + gestione_pickinglist_flow.md + async_db_flow.md + async_msssql_query_flow.md + gestione_aree_frame_async_flow.md + async_loop_singleton_flow.md diff --git a/docs/_build/html/_sources/flows/layout_window_flow.md.txt b/docs/_build/html/_sources/flows/layout_window_flow.md.txt new file mode 100644 index 0000000..818d079 --- /dev/null +++ b/docs/_build/html/_sources/flows/layout_window_flow.md.txt @@ -0,0 +1,61 @@ +# `layout_window.py` + +## Scopo + +Questo modulo visualizza il layout delle corsie come matrice di celle, mostra +lo stato di occupazione, consente di cercare una UDC e permette l'export della +matrice. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_layout_window()"] --> B["Crea o riporta in primo piano LayoutWindow"] + B --> C["LayoutWindow.__init__()"] + C --> D["Costruisce toolbar, host matrice, statistiche"] + D --> E["_load_corsie()"] + E --> F["AsyncRunner.run(query_json SQL corsie)"] + F --> G["_on_select() sulla corsia iniziale"] + G --> H["_load_matrix(corsia)"] + H --> I["AsyncRunner.run(query_json SQL matrice)"] + I --> J["_rebuild_matrix()"] + J --> K["_refresh_stats()"] +``` + +## Ricerca UDC + +```{mermaid} +flowchart TD + A["Utente inserisce barcode"] --> B["_search_udc()"] + B --> C["query_json ricerca pallet -> corsia/colonna/fila"] + C --> D{"UDC trovata?"} + D -- No --> E["Messaggio informativo"] + D -- Si --> F["Seleziona corsia in listbox"] + F --> G["_load_matrix(corsia)"] + G --> H["_rebuild_matrix()"] + H --> I["_highlight_cell_by_labels()"] +``` + +## Schema di chiamata essenziale + +```{mermaid} +flowchart LR + Init["__init__"] --> Top["_build_top"] + Init --> Host["_build_matrix_host"] + Init --> Stats["_build_stats"] + Init --> LoadCorsie["_load_corsie"] + LoadCorsie --> Select["_on_select"] + Select --> LoadMatrix["_load_matrix"] + LoadMatrix --> Rebuild["_rebuild_matrix"] + Rebuild --> RefreshStats["_refresh_stats"] + Search["_search_udc"] --> LoadMatrix + Export["_export_xlsx"] --> MatrixState["matrix_state / fila_txt / col_txt / udc1"] +``` + +## Note + +- Il modulo usa un token `_req_counter` per evitare che risposte async vecchie + aggiornino la UI fuori ordine. +- La statistica globale viene ricalcolata da query SQL, mentre quella della + corsia corrente usa la matrice già caricata in memoria. +- `destroy()` marca la finestra come non più attiva per evitare callback tardive. diff --git a/docs/_build/html/_sources/flows/main_flow.md.txt b/docs/_build/html/_sources/flows/main_flow.md.txt new file mode 100644 index 0000000..cca4eb6 --- /dev/null +++ b/docs/_build/html/_sources/flows/main_flow.md.txt @@ -0,0 +1,45 @@ +# `main.py` + +## Scopo + +`main.py` è il punto di ingresso dell'applicazione desktop. Inizializza il loop +asincrono condiviso, crea il client database condiviso e costruisce il launcher +con i pulsanti che aprono i moduli operativi. + +## Flusso principale + +```{mermaid} +flowchart TD + A["Avvio di main.py"] --> B["Configura policy asyncio su Windows"] + B --> C["Ottiene loop globale con get_global_loop()"] + C --> D["Imposta il loop come default"] + D --> E["Costruisce DSN SQL Server"] + E --> F["Crea AsyncMSSQLClient condiviso"] + F --> G["Istanzia Launcher"] + G --> H["Mostra finestra principale"] + H --> I{"Click su un pulsante"} + I --> J["open_reset_corsie_window()"] + I --> K["open_layout_window()"] + I --> L["open_celle_multiple_window()"] + I --> M["open_search_window()"] + I --> N["open_pickinglist_window()"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Launcher["Launcher.__init__"] --> Reset["open_reset_corsie_window"] + Launcher --> Layout["open_layout_window"] + Launcher --> Ghost["open_celle_multiple_window"] + Launcher --> Search["open_search_window"] + Launcher --> Pick["open_pickinglist_window"] + Pick --> PickFactory["create_pickinglist_frame"] +``` + +## Note + +- `db_app` viene creato una sola volta e poi passato a tutte le finestre. +- Alla chiusura del launcher viene chiamato `db_app.dispose()` sul loop globale. +- `open_pickinglist_window()` costruisce la finestra in modo nascosto e la rende + visibile solo a layout pronto, per ridurre lo sfarfallio iniziale. diff --git a/docs/_build/html/_sources/flows/reset_corsie_flow.md.txt b/docs/_build/html/_sources/flows/reset_corsie_flow.md.txt new file mode 100644 index 0000000..6ebc169 --- /dev/null +++ b/docs/_build/html/_sources/flows/reset_corsie_flow.md.txt @@ -0,0 +1,45 @@ +# `reset_corsie.py` + +## Scopo + +Questo modulo mostra il riepilogo di una corsia e permette, dopo doppia +conferma, di cancellare i record di `MagazziniPallet` collegati a quella corsia. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_reset_corsie_window()"] --> B["ResetCorsieWindow.__init__()"] + B --> C["_build_ui()"] + C --> D["_load_corsie()"] + D --> E["query_json SQL_CORSIE"] + E --> F["Seleziona corsia iniziale"] + F --> G["refresh()"] + G --> H["query_json SQL_RIEPILOGO"] + G --> I["query_json SQL_DETTAGLIO"] + H --> J["Aggiorna contatori"] + I --> K["Aggiorna tree celle occupate"] +``` + +## Flusso distruttivo + +```{mermaid} +flowchart TD + A["Click su Svuota corsia"] --> B["_ask_reset()"] + B --> C["query_json SQL_COUNT_DELETE"] + C --> D{"Record da cancellare > 0?"} + D -- No --> E["Messaggio: niente da rimuovere"] + D -- Si --> F["Richiesta conferma testuale"] + F --> G{"Testo corretto?"} + G -- No --> H["Annulla operazione"] + G -- Si --> I["_do_reset(corsia)"] + I --> J["query_json SQL_DELETE"] + J --> K["Messaggio completato"] + K --> L["refresh()"] +``` + +## Note + +- È il modulo più delicato lato operazioni, perché esegue `DELETE`. +- La finestra separa chiaramente fase di ispezione e fase distruttiva. +- Tutte le query passano dal client condiviso tramite `AsyncRunner`. diff --git a/docs/_build/html/_sources/flows/search_pallets_flow.md.txt b/docs/_build/html/_sources/flows/search_pallets_flow.md.txt new file mode 100644 index 0000000..dffecb1 --- /dev/null +++ b/docs/_build/html/_sources/flows/search_pallets_flow.md.txt @@ -0,0 +1,43 @@ +# `search_pallets.py` + +## Scopo + +Questo modulo consente di cercare pallet/UDC, lotti e codici prodotto su tutto +il magazzino e di esportare i risultati. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_search_window()"] --> B["SearchWindow.__init__()"] + B --> C["_build_ui()"] + C --> D["Utente compila filtri"] + D --> E["_do_search()"] + E --> F{"Filtri vuoti?"} + F -- Si --> G["Richiesta conferma ricerca globale"] + F -- No --> H["Prepara parametri SQL"] + G --> H + H --> I["AsyncRunner.run(query_json SQL_SEARCH)"] + I --> J["_ok()"] + J --> K["Popola Treeview"] + K --> L["Eventuale reset campi"] +``` + +## Ordinamento ed export + +```{mermaid} +flowchart TD + A["Doppio click su header"] --> B["_on_heading_double_click()"] + B --> C["_sort_by_column()"] + C --> D["Riordina righe del Treeview"] + + E["Click Export XLSX"] --> F["_export_xlsx()"] + F --> G["Legge righe visibili"] + G --> H["Scrive workbook Excel"] +``` + +## Note + +- Il modulo usa `Treeview` come backend principale. +- Le ricerche possono essere molto ampie: per questo, senza filtri, viene chiesta conferma. +- `IDCella = 9999` viene evidenziata con uno stile dedicato. diff --git a/docs/_build/html/_sources/flows/view_celle_multiple_flow.md.txt b/docs/_build/html/_sources/flows/view_celle_multiple_flow.md.txt new file mode 100644 index 0000000..5dd4187 --- /dev/null +++ b/docs/_build/html/_sources/flows/view_celle_multiple_flow.md.txt @@ -0,0 +1,58 @@ +# `view_celle_multiple.py` + +## Scopo + +Questo modulo esplora le celle che contengono più pallet del previsto, +organizzando i risultati in un albero: + +- corsia +- cella duplicata +- pallet contenuti nella cella + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_celle_multiple_window()"] --> B["CelleMultipleWindow.__init__()"] + B --> C["_build_layout()"] + C --> D["_bind_events()"] + D --> E["refresh_all()"] + E --> F["_load_corsie()"] + E --> G["_load_riepilogo()"] + F --> H["query_json SQL_CORSIE"] + G --> I["query_json SQL_RIEPILOGO_PERCENTUALI"] + H --> J["_fill_corsie()"] + I --> K["_fill_riepilogo()"] +``` + +## Lazy loading dell'albero + +```{mermaid} +flowchart TD + A["Espansione nodo tree"] --> B["_on_open_node()"] + B --> C{"Nodo corsia o nodo cella?"} + C -- Corsia --> D["_load_celle_for_corsia()"] + D --> E["query_json SQL_CELLE_DUP_PER_CORSIA"] + E --> F["_fill_celle()"] + C -- Cella --> G["_load_pallet_for_cella()"] + G --> H["query_json SQL_PALLET_IN_CELLA"] + H --> I["_fill_pallet()"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Refresh["refresh_all"] --> Corsie["_load_corsie"] + Refresh --> Riep["_load_riepilogo"] + Open["_on_open_node"] --> LoadCelle["_load_celle_for_corsia"] + Open --> LoadPallet["_load_pallet_for_cella"] + Export["export_to_xlsx"] --> Tree["tree dati dettaglio"] + Export --> Sum["sum_tbl riepilogo"] +``` + +## Note + +- L'albero è caricato a richiesta, non tutto in una sola query. +- Questo riduce il costo iniziale e rende il modulo più scalabile. +- L'export legge sia il dettaglio dell'albero sia la tabella di riepilogo. diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 0000000..7b1fe98 --- /dev/null +++ b/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,16 @@ +Warehouse Documentation +======================= + +Questa documentazione raccoglie: + +- riferimento API generato dal codice Python; +- diagrammi di flusso in Markdown/Mermaid; +- vista architetturale complessiva del progetto. + +.. toctree:: + :maxdepth: 2 + :caption: Contenuti + + architecture + api_reference + flows/index diff --git a/docs/_build/html/_static/alabaster.css b/docs/_build/html/_static/alabaster.css new file mode 100644 index 0000000..7e75bf8 --- /dev/null +++ b/docs/_build/html/_static/alabaster.css @@ -0,0 +1,663 @@ +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar { + max-height: 100%; + overflow-y: auto; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox { + margin: 1em 0; +} + +div.sphinxsidebar .search > div { + display: table-cell; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: unset; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + +@media screen and (max-width: 940px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.sphinxsidebar { + display: block; + float: none; + width: unset; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + min-width: auto; /* fixes width on small screens, breaks .hll */ + padding: 0; + } + + .hll { + /* "fixes" the breakage */ + width: max-content; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Hide ugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} + +img.github { + position: absolute; + top: 0; + border: 0; + right: 0; +} \ No newline at end of file diff --git a/docs/_build/html/_static/base-stemmer.js b/docs/_build/html/_static/base-stemmer.js new file mode 100644 index 0000000..e6fa0c4 --- /dev/null +++ b/docs/_build/html/_static/base-stemmer.js @@ -0,0 +1,476 @@ +// @ts-check + +/**@constructor*/ +BaseStemmer = function() { + /** @protected */ + this.current = ''; + this.cursor = 0; + this.limit = 0; + this.limit_backward = 0; + this.bra = 0; + this.ket = 0; + + /** + * @param {string} value + */ + this.setCurrent = function(value) { + this.current = value; + this.cursor = 0; + this.limit = this.current.length; + this.limit_backward = 0; + this.bra = this.cursor; + this.ket = this.limit; + }; + + /** + * @return {string} + */ + this.getCurrent = function() { + return this.current; + }; + + /** + * @param {BaseStemmer} other + */ + this.copy_from = function(other) { + /** @protected */ + this.current = other.current; + this.cursor = other.cursor; + this.limit = other.limit; + this.limit_backward = other.limit_backward; + this.bra = other.bra; + this.ket = other.ket; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor++; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) + return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) + return true; + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor--; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true; + this.cursor--; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) { + this.cursor++; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) { + this.cursor++; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) { + this.cursor--; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) { + this.cursor--; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor--; + } + return false; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s = function(s) + { + /** @protected */ + if (this.limit - this.cursor < s.length) return false; + if (this.current.slice(this.cursor, this.cursor + s.length) != s) + { + return false; + } + this.cursor += s.length; + return true; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s_b = function(s) + { + /** @protected */ + if (this.cursor - this.limit_backward < s.length) return false; + if (this.current.slice(this.cursor - s.length, this.cursor) != s) + { + return false; + } + this.cursor -= s.length; + return true; + }; + + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among = function(v) + { + /** @protected */ + var i = 0; + var j = v.length; + + var c = this.cursor; + var l = this.limit; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >>> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; // smaller + // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional) + var w = v[k]; + var i2; + for (i2 = common; i2 < w[0].length; i2++) + { + if (c + common == l) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; // v->s has been inspected + if (j == i) break; // only one item in v + + // - but now we need to go round once more to get + // v->s inspected. This looks messy, but is actually + // the optimal approach. + + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c + w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c + w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + // find_among_b is for backwards processing. Same comments apply + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among_b = function(v) + { + /** @protected */ + var i = 0; + var j = v.length + + var c = this.cursor; + var lb = this.limit_backward; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; + var w = v[k]; + var i2; + for (i2 = w[0].length - 1 - common; i2 >= 0; i2--) + { + if (c - common == lb) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; + if (j == i) break; + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c - w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c - w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + /* to replace chars between c_bra and c_ket in this.current by the + * chars in s. + */ + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + * @return {number} + */ + this.replace_s = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = s.length - (c_ket - c_bra); + this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket); + this.limit += adjustment; + if (this.cursor >= c_ket) this.cursor += adjustment; + else if (this.cursor > c_bra) this.cursor = c_bra; + return adjustment; + }; + + /** + * @return {boolean} + */ + this.slice_check = function() + { + /** @protected */ + if (this.bra < 0 || + this.bra > this.ket || + this.ket > this.limit || + this.limit > this.current.length) + { + return false; + } + return true; + }; + + /** + * @param {number} c_bra + * @return {boolean} + */ + this.slice_from = function(s) + { + /** @protected */ + var result = false; + if (this.slice_check()) + { + this.replace_s(this.bra, this.ket, s); + result = true; + } + return result; + }; + + /** + * @return {boolean} + */ + this.slice_del = function() + { + /** @protected */ + return this.slice_from(""); + }; + + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + */ + this.insert = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = this.replace_s(c_bra, c_ket, s); + if (c_bra <= this.bra) this.bra += adjustment; + if (c_bra <= this.ket) this.ket += adjustment; + }; + + /** + * @return {string} + */ + this.slice_to = function() + { + /** @protected */ + var result = ''; + if (this.slice_check()) + { + result = this.current.slice(this.bra, this.ket); + } + return result; + }; + + /** + * @return {string} + */ + this.assign_to = function() + { + /** @protected */ + return this.current.slice(0, this.limit); + }; +}; diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 0000000..0028826 --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,906 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: inherit; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/custom.css b/docs/_build/html/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/docs/_build/html/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/docs/_build/html/_static/doctools.js b/docs/_build/html/_static/doctools.js new file mode 100644 index 0000000..807cdb1 --- /dev/null +++ b/docs/_build/html/_static/doctools.js @@ -0,0 +1,150 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})`, + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)), + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS + && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/_build/html/_static/documentation_options.js b/docs/_build/html/_static/documentation_options.js new file mode 100644 index 0000000..d1f2291 --- /dev/null +++ b/docs/_build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.0.1', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/_build/html/_static/english-stemmer.js b/docs/_build/html/_static/english-stemmer.js new file mode 100644 index 0000000..056760e --- /dev/null +++ b/docs/_build/html/_static/english-stemmer.js @@ -0,0 +1,1066 @@ +// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/ + +/**@constructor*/ +var EnglishStemmer = function() { + var base = new BaseStemmer(); + + /** @const */ var a_0 = [ + ["arsen", -1, -1], + ["commun", -1, -1], + ["emerg", -1, -1], + ["gener", -1, -1], + ["later", -1, -1], + ["organ", -1, -1], + ["past", -1, -1], + ["univers", -1, -1] + ]; + + /** @const */ var a_1 = [ + ["'", -1, 1], + ["'s'", 0, 1], + ["'s", -1, 1] + ]; + + /** @const */ var a_2 = [ + ["ied", -1, 2], + ["s", -1, 3], + ["ies", 1, 2], + ["sses", 1, 1], + ["ss", 1, -1], + ["us", 1, -1] + ]; + + /** @const */ var a_3 = [ + ["succ", -1, 1], + ["proc", -1, 1], + ["exc", -1, 1] + ]; + + /** @const */ var a_4 = [ + ["even", -1, 2], + ["cann", -1, 2], + ["inn", -1, 2], + ["earr", -1, 2], + ["herr", -1, 2], + ["out", -1, 2], + ["y", -1, 1] + ]; + + /** @const */ var a_5 = [ + ["", -1, -1], + ["ed", 0, 2], + ["eed", 1, 1], + ["ing", 0, 3], + ["edly", 0, 2], + ["eedly", 4, 1], + ["ingly", 0, 2] + ]; + + /** @const */ var a_6 = [ + ["", -1, 3], + ["bb", 0, 2], + ["dd", 0, 2], + ["ff", 0, 2], + ["gg", 0, 2], + ["bl", 0, 1], + ["mm", 0, 2], + ["nn", 0, 2], + ["pp", 0, 2], + ["rr", 0, 2], + ["at", 0, 1], + ["tt", 0, 2], + ["iz", 0, 1] + ]; + + /** @const */ var a_7 = [ + ["anci", -1, 3], + ["enci", -1, 2], + ["ogi", -1, 14], + ["li", -1, 16], + ["bli", 3, 12], + ["abli", 4, 4], + ["alli", 3, 8], + ["fulli", 3, 9], + ["lessli", 3, 15], + ["ousli", 3, 10], + ["entli", 3, 5], + ["aliti", -1, 8], + ["biliti", -1, 12], + ["iviti", -1, 11], + ["tional", -1, 1], + ["ational", 14, 7], + ["alism", -1, 8], + ["ation", -1, 7], + ["ization", 17, 6], + ["izer", -1, 6], + ["ator", -1, 7], + ["iveness", -1, 11], + ["fulness", -1, 9], + ["ousness", -1, 10], + ["ogist", -1, 13] + ]; + + /** @const */ var a_8 = [ + ["icate", -1, 4], + ["ative", -1, 6], + ["alize", -1, 3], + ["iciti", -1, 4], + ["ical", -1, 4], + ["tional", -1, 1], + ["ational", 5, 2], + ["ful", -1, 5], + ["ness", -1, 5] + ]; + + /** @const */ var a_9 = [ + ["ic", -1, 1], + ["ance", -1, 1], + ["ence", -1, 1], + ["able", -1, 1], + ["ible", -1, 1], + ["ate", -1, 1], + ["ive", -1, 1], + ["ize", -1, 1], + ["iti", -1, 1], + ["al", -1, 1], + ["ism", -1, 1], + ["ion", -1, 2], + ["er", -1, 1], + ["ous", -1, 1], + ["ant", -1, 1], + ["ent", -1, 1], + ["ment", 15, 1], + ["ement", 16, 1] + ]; + + /** @const */ var a_10 = [ + ["e", -1, 1], + ["l", -1, 2] + ]; + + /** @const */ var a_11 = [ + ["andes", -1, -1], + ["atlas", -1, -1], + ["bias", -1, -1], + ["cosmos", -1, -1], + ["early", -1, 5], + ["gently", -1, 3], + ["howe", -1, -1], + ["idly", -1, 2], + ["news", -1, -1], + ["only", -1, 6], + ["singly", -1, 7], + ["skies", -1, 1], + ["sky", -1, -1], + ["ugly", -1, 4] + ]; + + /** @const */ var /** Array */ g_aeo = [17, 64]; + + /** @const */ var /** Array */ g_v = [17, 65, 16, 1]; + + /** @const */ var /** Array */ g_v_WXY = [1, 17, 65, 208, 1]; + + /** @const */ var /** Array */ g_valid_LI = [55, 141, 2]; + + var /** boolean */ B_Y_found = false; + var /** number */ I_p2 = 0; + var /** number */ I_p1 = 0; + + + /** @return {boolean} */ + function r_prelude() { + B_Y_found = false; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + base.bra = base.cursor; + if (!(base.eq_s("'"))) + { + break lab0; + } + base.ket = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.cursor = v_1; + /** @const */ var /** number */ v_2 = base.cursor; + lab1: { + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab1; + } + base.ket = base.cursor; + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + } + base.cursor = v_2; + /** @const */ var /** number */ v_3 = base.cursor; + lab2: { + while(true) + { + /** @const */ var /** number */ v_4 = base.cursor; + lab3: { + golab4: while(true) + { + /** @const */ var /** number */ v_5 = base.cursor; + lab5: { + if (!(base.in_grouping(g_v, 97, 121))) + { + break lab5; + } + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab5; + } + base.ket = base.cursor; + base.cursor = v_5; + break golab4; + } + base.cursor = v_5; + if (base.cursor >= base.limit) + { + break lab3; + } + base.cursor++; + } + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + continue; + } + base.cursor = v_4; + break; + } + } + base.cursor = v_3; + return true; + }; + + /** @return {boolean} */ + function r_mark_regions() { + I_p1 = base.limit; + I_p2 = base.limit; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + lab1: { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + if (base.find_among(a_0) == 0) + { + break lab2; + } + break lab1; + } + base.cursor = v_2; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + } + I_p1 = base.cursor; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + I_p2 = base.cursor; + } + base.cursor = v_1; + return true; + }; + + /** @return {boolean} */ + function r_shortv() { + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.out_grouping_b(g_v_WXY, 89, 121))) + { + break lab1; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + lab2: { + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (base.cursor > base.limit_backward) + { + break lab2; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("past"))) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_R1() { + return I_p1 <= base.cursor; + }; + + /** @return {boolean} */ + function r_R2() { + return I_p2 <= base.cursor; + }; + + /** @return {boolean} */ + function r_Step_1a() { + var /** number */ among_var; + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab0: { + base.ket = base.cursor; + if (base.find_among_b(a_1) == 0) + { + base.cursor = base.limit - v_1; + break lab0; + } + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.ket = base.cursor; + among_var = base.find_among_b(a_2); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + if (!base.slice_from("ss")) + { + return false; + } + break; + case 2: + lab1: { + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + { + /** @const */ var /** number */ c1 = base.cursor - 2; + if (c1 < base.limit_backward) + { + break lab2; + } + base.cursor = c1; + } + if (!base.slice_from("i")) + { + return false; + } + break lab1; + } + base.cursor = base.limit - v_2; + if (!base.slice_from("ie")) + { + return false; + } + } + break; + case 3: + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1b() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_5); + base.bra = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + switch (among_var) { + case 1: + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + lab3: { + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + lab4: { + if (base.find_among_b(a_3) == 0) + { + break lab4; + } + if (base.cursor > base.limit_backward) + { + break lab4; + } + break lab3; + } + base.cursor = base.limit - v_3; + if (!r_R1()) + { + break lab2; + } + if (!base.slice_from("ee")) + { + return false; + } + } + } + base.cursor = base.limit - v_2; + break; + case 2: + break lab1; + case 3: + among_var = base.find_among_b(a_4); + if (among_var == 0) + { + break lab1; + } + switch (among_var) { + case 1: + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (base.cursor > base.limit_backward) + { + break lab1; + } + base.cursor = base.limit - v_4; + base.bra = base.cursor; + if (!base.slice_from("ie")) + { + return false; + } + break; + case 2: + if (base.cursor > base.limit_backward) + { + break lab1; + } + break; + } + break; + } + break lab0; + } + base.cursor = base.limit - v_1; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + base.cursor = base.limit - v_5; + if (!base.slice_del()) + { + return false; + } + base.ket = base.cursor; + base.bra = base.cursor; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + among_var = base.find_among_b(a_6); + switch (among_var) { + case 1: + if (!base.slice_from("e")) + { + return false; + } + return false; + case 2: + { + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + lab5: { + if (!(base.in_grouping_b(g_aeo, 97, 111))) + { + break lab5; + } + if (base.cursor > base.limit_backward) + { + break lab5; + } + return false; + } + base.cursor = base.limit - v_7; + } + break; + case 3: + if (base.cursor != I_p1) + { + return false; + } + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + if (!r_shortv()) + { + return false; + } + base.cursor = base.limit - v_8; + if (!base.slice_from("e")) + { + return false; + } + return false; + } + base.cursor = base.limit - v_6; + base.ket = base.cursor; + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1c() { + base.ket = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("y"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("Y"))) + { + return false; + } + } + base.bra = base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + return false; + } + lab2: { + if (base.cursor > base.limit_backward) + { + break lab2; + } + return false; + } + if (!base.slice_from("i")) + { + return false; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_2() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_7); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ence")) + { + return false; + } + break; + case 3: + if (!base.slice_from("ance")) + { + return false; + } + break; + case 4: + if (!base.slice_from("able")) + { + return false; + } + break; + case 5: + if (!base.slice_from("ent")) + { + return false; + } + break; + case 6: + if (!base.slice_from("ize")) + { + return false; + } + break; + case 7: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 8: + if (!base.slice_from("al")) + { + return false; + } + break; + case 9: + if (!base.slice_from("ful")) + { + return false; + } + break; + case 10: + if (!base.slice_from("ous")) + { + return false; + } + break; + case 11: + if (!base.slice_from("ive")) + { + return false; + } + break; + case 12: + if (!base.slice_from("ble")) + { + return false; + } + break; + case 13: + if (!base.slice_from("og")) + { + return false; + } + break; + case 14: + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_from("og")) + { + return false; + } + break; + case 15: + if (!base.slice_from("less")) + { + return false; + } + break; + case 16: + if (!(base.in_grouping_b(g_valid_LI, 99, 116))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_3() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_8); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 3: + if (!base.slice_from("al")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ic")) + { + return false; + } + break; + case 5: + if (!base.slice_del()) + { + return false; + } + break; + case 6: + if (!r_R2()) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_4() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_9); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R2()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_del()) + { + return false; + } + break; + case 2: + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("s"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("t"))) + { + return false; + } + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_5() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_10); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + lab0: { + lab1: { + if (!r_R2()) + { + break lab1; + } + break lab0; + } + if (!r_R1()) + { + return false; + } + { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab2: { + if (!r_shortv()) + { + break lab2; + } + return false; + } + base.cursor = base.limit - v_1; + } + } + if (!base.slice_del()) + { + return false; + } + break; + case 2: + if (!r_R2()) + { + return false; + } + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_exception1() { + var /** number */ among_var; + base.bra = base.cursor; + among_var = base.find_among(a_11); + if (among_var == 0) + { + return false; + } + base.ket = base.cursor; + if (base.cursor < base.limit) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("sky")) + { + return false; + } + break; + case 2: + if (!base.slice_from("idl")) + { + return false; + } + break; + case 3: + if (!base.slice_from("gentl")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ugli")) + { + return false; + } + break; + case 5: + if (!base.slice_from("earli")) + { + return false; + } + break; + case 6: + if (!base.slice_from("onli")) + { + return false; + } + break; + case 7: + if (!base.slice_from("singl")) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_postlude() { + if (!B_Y_found) + { + return false; + } + while(true) + { + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + golab1: while(true) + { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + base.bra = base.cursor; + if (!(base.eq_s("Y"))) + { + break lab2; + } + base.ket = base.cursor; + base.cursor = v_2; + break golab1; + } + base.cursor = v_2; + if (base.cursor >= base.limit) + { + break lab0; + } + base.cursor++; + } + if (!base.slice_from("y")) + { + return false; + } + continue; + } + base.cursor = v_1; + break; + } + return true; + }; + + this.stem = /** @return {boolean} */ function() { + lab0: { + /** @const */ var /** number */ v_1 = base.cursor; + lab1: { + if (!r_exception1()) + { + break lab1; + } + break lab0; + } + base.cursor = v_1; + lab2: { + { + /** @const */ var /** number */ v_2 = base.cursor; + lab3: { + { + /** @const */ var /** number */ c1 = base.cursor + 3; + if (c1 > base.limit) + { + break lab3; + } + base.cursor = c1; + } + break lab2; + } + base.cursor = v_2; + } + break lab0; + } + base.cursor = v_1; + r_prelude(); + r_mark_regions(); + base.limit_backward = base.cursor; base.cursor = base.limit; + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + r_Step_1a(); + base.cursor = base.limit - v_3; + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + r_Step_1b(); + base.cursor = base.limit - v_4; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + r_Step_1c(); + base.cursor = base.limit - v_5; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + r_Step_2(); + base.cursor = base.limit - v_6; + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + r_Step_3(); + base.cursor = base.limit - v_7; + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + r_Step_4(); + base.cursor = base.limit - v_8; + /** @const */ var /** number */ v_9 = base.limit - base.cursor; + r_Step_5(); + base.cursor = base.limit - v_9; + base.cursor = base.limit_backward; + /** @const */ var /** number */ v_10 = base.cursor; + r_postlude(); + base.cursor = v_10; + } + return true; + }; + + /**@return{string}*/ + this['stemWord'] = function(/**string*/word) { + base.setCurrent(word); + this.stem(); + return base.getCurrent(); + }; +}; diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/_build/html/_static/file.png differ diff --git a/docs/_build/html/_static/github-banner.svg b/docs/_build/html/_static/github-banner.svg new file mode 100644 index 0000000..c47d9dc --- /dev/null +++ b/docs/_build/html/_static/github-banner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/_build/html/_static/language_data.js b/docs/_build/html/_static/language_data.js new file mode 100644 index 0000000..5776786 --- /dev/null +++ b/docs/_build/html/_static/language_data.js @@ -0,0 +1,13 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the set of stopwords, stemmer, scorer and splitter. + */ + +const stopwords = new Set(["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]); +window.stopwords = stopwords; // Export to global scope + + +/* Non-minified versions are copied as separate JavaScript files, if available */ +BaseStemmer=function(){this.current="",this.cursor=0,this.limit=0,this.limit_backward=0,this.bra=0,this.ket=0,this.setCurrent=function(t){this.current=t,this.cursor=0,this.limit=this.current.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},this.getCurrent=function(){return this.current},this.copy_from=function(t){this.current=t.current,this.cursor=t.cursor,this.limit=t.limit,this.limit_backward=t.limit_backward,this.bra=t.bra,this.ket=t.ket},this.in_grouping=function(t,r,i){return!(this.cursor>=this.limit||i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i))||(this.cursor++,0))},this.go_in_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.in_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward||i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i))||(this.cursor--,0))},this.go_in_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(i>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.out_grouping=function(t,r,i){return!(this.cursor>=this.limit)&&(i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i)))&&(this.cursor++,!0)},this.go_out_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.out_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward)&&(i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i)))&&(this.cursor--,!0)},this.go_out_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(s<=i&&r<=s&&0!=(t[(s-=r)>>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.eq_s=function(t){return!(this.limit-this.cursor>>1),o=0,a=e=(l=t[r])[0].length){if(this.cursor=s+l[0].length,l.length<4)return l[2];var g=l[3](this);if(this.cursor=s+l[0].length,g)return l[2]}}while(0<=(r=l[1]));return 0},this.find_among_b=function(t){for(var r=0,i=t.length,s=this.cursor,h=this.limit_backward,e=0,n=0,c=!1;;){for(var u,o=r+(i-r>>1),a=0,l=e=(u=t[r])[0].length){if(this.cursor=s-u[0].length,u.length<4)return u[2];var g=u[3](this);if(this.cursor=s-u[0].length,g)return u[2]}}while(0<=(r=u[1]));return 0},this.replace_s=function(t,r,i){var s=i.length-(r-t);return this.current=this.current.slice(0,t)+i+this.current.slice(r),this.limit+=s,this.cursor>=r?this.cursor+=s:this.cursor>t&&(this.cursor=t),s},this.slice_check=function(){return!(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>this.current.length)},this.slice_from=function(t){var r=!1;return this.slice_check()&&(this.replace_s(this.bra,this.ket,t),r=!0),r},this.slice_del=function(){return this.slice_from("")},this.insert=function(t,r,i){r=this.replace_s(t,r,i);t<=this.bra&&(this.bra+=r),t<=this.ket&&(this.ket+=r)},this.slice_to=function(){var t="";return t=this.slice_check()?this.current.slice(this.bra,this.ket):t},this.assign_to=function(){return this.current.slice(0,this.limit)}}; +var EnglishStemmer=function(){var a=new BaseStemmer,c=[["arsen",-1,-1],["commun",-1,-1],["emerg",-1,-1],["gener",-1,-1],["later",-1,-1],["organ",-1,-1],["past",-1,-1],["univers",-1,-1]],o=[["'",-1,1],["'s'",0,1],["'s",-1,1]],u=[["ied",-1,2],["s",-1,3],["ies",1,2],["sses",1,1],["ss",1,-1],["us",1,-1]],t=[["succ",-1,1],["proc",-1,1],["exc",-1,1]],l=[["even",-1,2],["cann",-1,2],["inn",-1,2],["earr",-1,2],["herr",-1,2],["out",-1,2],["y",-1,1]],n=[["",-1,-1],["ed",0,2],["eed",1,1],["ing",0,3],["edly",0,2],["eedly",4,1],["ingly",0,2]],f=[["",-1,3],["bb",0,2],["dd",0,2],["ff",0,2],["gg",0,2],["bl",0,1],["mm",0,2],["nn",0,2],["pp",0,2],["rr",0,2],["at",0,1],["tt",0,2],["iz",0,1]],_=[["anci",-1,3],["enci",-1,2],["ogi",-1,14],["li",-1,16],["bli",3,12],["abli",4,4],["alli",3,8],["fulli",3,9],["lessli",3,15],["ousli",3,10],["entli",3,5],["aliti",-1,8],["biliti",-1,12],["iviti",-1,11],["tional",-1,1],["ational",14,7],["alism",-1,8],["ation",-1,7],["ization",17,6],["izer",-1,6],["ator",-1,7],["iveness",-1,11],["fulness",-1,9],["ousness",-1,10],["ogist",-1,13]],m=[["icate",-1,4],["ative",-1,6],["alize",-1,3],["iciti",-1,4],["ical",-1,4],["tional",-1,1],["ational",5,2],["ful",-1,5],["ness",-1,5]],b=[["ic",-1,1],["ance",-1,1],["ence",-1,1],["able",-1,1],["ible",-1,1],["ate",-1,1],["ive",-1,1],["ize",-1,1],["iti",-1,1],["al",-1,1],["ism",-1,1],["ion",-1,2],["er",-1,1],["ous",-1,1],["ant",-1,1],["ent",-1,1],["ment",15,1],["ement",16,1]],k=[["e",-1,1],["l",-1,2]],g=[["andes",-1,-1],["atlas",-1,-1],["bias",-1,-1],["cosmos",-1,-1],["early",-1,5],["gently",-1,3],["howe",-1,-1],["idly",-1,2],["news",-1,-1],["only",-1,6],["singly",-1,7],["skies",-1,1],["sky",-1,-1],["ugly",-1,4]],d=[17,64],v=[17,65,16,1],i=[1,17,65,208,1],w=[55,141,2],p=!1,y=0,h=0;function q(){var r=a.limit-a.cursor;return!!(a.out_grouping_b(i,89,121)&&a.in_grouping_b(v,97,121)&&a.out_grouping_b(v,97,121)||(a.cursor=a.limit-r,a.out_grouping_b(v,97,121)&&a.in_grouping_b(v,97,121)&&!(a.cursor>a.limit_backward))||(a.cursor=a.limit-r,a.eq_s_b("past")))}function z(){return h<=a.cursor}function Y(){return y<=a.cursor}this.stem=function(){var r=a.cursor;if(!(()=>{var r;if(a.bra=a.cursor,0!=(r=a.find_among(g))&&(a.ket=a.cursor,!(a.cursora.limit)a.cursor=i;else{a.cursor=e,a.cursor=r,(()=>{p=!1;var r=a.cursor;if(a.bra=a.cursor,!a.eq_s("'")||(a.ket=a.cursor,a.slice_del())){a.cursor=r;r=a.cursor;if(a.bra=a.cursor,a.eq_s("y")){if(a.ket=a.cursor,!a.slice_from("Y"))return;p=!0}a.cursor=r;for(r=a.cursor;;){var i=a.cursor;r:{for(;;){var e=a.cursor;if(a.in_grouping(v,97,121)&&(a.bra=a.cursor,a.eq_s("y"))){a.ket=a.cursor,a.cursor=e;break}if(a.cursor=e,a.cursor>=a.limit)break r;a.cursor++}if(!a.slice_from("Y"))return;p=!0;continue}a.cursor=i;break}a.cursor=r}})(),h=a.limit,y=a.limit;i=a.cursor;r:{var s=a.cursor;if(0==a.find_among(c)){if(a.cursor=s,!a.go_out_grouping(v,97,121))break r;if(a.cursor++,!a.go_in_grouping(v,97,121))break r;a.cursor++}h=a.cursor,a.go_out_grouping(v,97,121)&&(a.cursor++,a.go_in_grouping(v,97,121))&&(a.cursor++,y=a.cursor)}a.cursor=i,a.limit_backward=a.cursor,a.cursor=a.limit;var e=a.limit-a.cursor,r=((()=>{var r=a.limit-a.cursor;if(a.ket=a.cursor,0==a.find_among_b(o))a.cursor=a.limit-r;else if(a.bra=a.cursor,!a.slice_del())return;if(a.ket=a.cursor,0!=(r=a.find_among_b(u)))switch(a.bra=a.cursor,r){case 1:if(a.slice_from("ss"))break;return;case 2:r:{var i=a.limit-a.cursor,e=a.cursor-2;if(!(e{a.ket=a.cursor,o=a.find_among_b(n),a.bra=a.cursor;r:{var r=a.limit-a.cursor;i:{switch(o){case 1:var i=a.limit-a.cursor;e:{var e=a.limit-a.cursor;if(0==a.find_among_b(t)||a.cursor>a.limit_backward){if(a.cursor=a.limit-e,!z())break e;if(!a.slice_from("ee"))return}}a.cursor=a.limit-i;break;case 2:break i;case 3:if(0==(o=a.find_among_b(l)))break i;switch(o){case 1:var s=a.limit-a.cursor;if(!a.out_grouping_b(v,97,121))break i;if(a.cursor>a.limit_backward)break i;if(a.cursor=a.limit-s,a.bra=a.cursor,a.slice_from("ie"))break;return;case 2:if(a.cursor>a.limit_backward)break i}}break r}a.cursor=a.limit-r;var c=a.limit-a.cursor;if(!a.go_out_grouping_b(v,97,121))return;if(a.cursor--,a.cursor=a.limit-c,!a.slice_del())return;a.ket=a.cursor,a.bra=a.cursor;var o,c=a.limit-a.cursor;switch(o=a.find_among_b(f)){case 1:return a.slice_from("e");case 2:var u=a.limit-a.cursor;if(a.in_grouping_b(d,97,111)&&!(a.cursor>a.limit_backward))return;a.cursor=a.limit-u;break;case 3:return a.cursor!=h||(u=a.limit-a.cursor,q()&&(a.cursor=a.limit-u,a.slice_from("e")))}if(a.cursor=a.limit-c,a.ket=a.cursor,a.cursor<=a.limit_backward)return;if(a.cursor--,a.bra=a.cursor,!a.slice_del())return}})(),a.cursor=a.limit-r,a.limit-a.cursor),r=(a.ket=a.cursor,e=a.limit-a.cursor,(a.eq_s_b("y")||(a.cursor=a.limit-e,a.eq_s_b("Y")))&&(a.bra=a.cursor,a.out_grouping_b(v,97,121))&&a.cursor>a.limit_backward&&a.slice_from("i"),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(_))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ence"))break;return;case 3:if(a.slice_from("ance"))break;return;case 4:if(a.slice_from("able"))break;return;case 5:if(a.slice_from("ent"))break;return;case 6:if(a.slice_from("ize"))break;return;case 7:if(a.slice_from("ate"))break;return;case 8:if(a.slice_from("al"))break;return;case 9:if(a.slice_from("ful"))break;return;case 10:if(a.slice_from("ous"))break;return;case 11:if(a.slice_from("ive"))break;return;case 12:if(a.slice_from("ble"))break;return;case 13:if(a.slice_from("og"))break;return;case 14:if(!a.eq_s_b("l"))return;if(a.slice_from("og"))break;return;case 15:if(a.slice_from("less"))break;return;case 16:if(!a.in_grouping_b(w,99,116))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.limit-a.cursor),i=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(m))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ate"))break;return;case 3:if(a.slice_from("al"))break;return;case 4:if(a.slice_from("ic"))break;return;case 5:if(a.slice_del())break;return;case 6:if(!Y())return;if(a.slice_del())break}})(),a.cursor=a.limit-e,a.limit-a.cursor),r=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(b))&&(a.bra=a.cursor,Y()))switch(r){case 1:if(a.slice_del())break;return;case 2:var i=a.limit-a.cursor;if(!a.eq_s_b("s")&&(a.cursor=a.limit-i,!a.eq_s_b("t")))return;if(a.slice_del())break}})(),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(k)))switch(a.bra=a.cursor,r){case 1:if(!Y()){if(!z())return;var i=a.limit-a.cursor;if(q())return;a.cursor=a.limit-i}if(a.slice_del())break;return;case 2:if(!Y())return;if(!a.eq_s_b("l"))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.cursor=a.limit_backward,a.cursor);(()=>{if(p)for(;;){var r=a.cursor;r:{for(;;){var i=a.cursor;if(a.bra=a.cursor,a.eq_s("Y")){a.ket=a.cursor,a.cursor=i;break}if(a.cursor=i,a.cursor>=a.limit)break r;a.cursor++}if(a.slice_from("y"))continue;return}a.cursor=r;break}})(),a.cursor=e}}return!0},this.stemWord=function(r){return a.setCurrent(r),this.stem(),a.getCurrent()}}; +window.Stemmer = EnglishStemmer; diff --git a/docs/_build/html/_static/minus.png b/docs/_build/html/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/docs/_build/html/_static/minus.png differ diff --git a/docs/_build/html/_static/plus.png b/docs/_build/html/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/docs/_build/html/_static/plus.png differ diff --git a/docs/_build/html/_static/pygments.css b/docs/_build/html/_static/pygments.css new file mode 100644 index 0000000..9392ddc --- /dev/null +++ b/docs/_build/html/_static/pygments.css @@ -0,0 +1,84 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8F5902; font-style: italic } /* Comment */ +.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */ +.highlight .g { color: #000 } /* Generic */ +.highlight .k { color: #004461; font-weight: bold } /* Keyword */ +.highlight .l { color: #000 } /* Literal */ +.highlight .n { color: #000 } /* Name */ +.highlight .o { color: #582800 } /* Operator */ +.highlight .x { color: #000 } /* Other */ +.highlight .p { color: #000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8F5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A40000 } /* Generic.Deleted */ +.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000 } /* Generic.EmphStrong */ +.highlight .gr { color: #EF2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888 } /* Generic.Output */ +.highlight .gp { color: #745334 } /* Generic.Prompt */ +.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000 } /* Literal.Date */ +.highlight .m { color: #900 } /* Literal.Number */ +.highlight .s { color: #4E9A06 } /* Literal.String */ +.highlight .na { color: #C4A000 } /* Name.Attribute */ +.highlight .nb { color: #004461 } /* Name.Builtin */ +.highlight .nc { color: #000 } /* Name.Class */ +.highlight .no { color: #000 } /* Name.Constant */ +.highlight .nd { color: #888 } /* Name.Decorator */ +.highlight .ni { color: #CE5C00 } /* Name.Entity */ +.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000 } /* Name.Function */ +.highlight .nl { color: #F57900 } /* Name.Label */ +.highlight .nn { color: #000 } /* Name.Namespace */ +.highlight .nx { color: #000 } /* Name.Other */ +.highlight .py { color: #000 } /* Name.Property */ +.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000 } /* Name.Variable */ +.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #F8F8F8 } /* Text.Whitespace */ +.highlight .mb { color: #900 } /* Literal.Number.Bin */ +.highlight .mf { color: #900 } /* Literal.Number.Float */ +.highlight .mh { color: #900 } /* Literal.Number.Hex */ +.highlight .mi { color: #900 } /* Literal.Number.Integer */ +.highlight .mo { color: #900 } /* Literal.Number.Oct */ +.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */ +.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4E9A06 } /* Literal.String.Char */ +.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */ +.highlight .se { color: #4E9A06 } /* Literal.String.Escape */ +.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4E9A06 } /* Literal.String.Other */ +.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */ +.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000 } /* Name.Function.Magic */ +.highlight .vc { color: #000 } /* Name.Variable.Class */ +.highlight .vg { color: #000 } /* Name.Variable.Global */ +.highlight .vi { color: #000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000 } /* Name.Variable.Magic */ +.highlight .il { color: #900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_build/html/_static/searchtools.js b/docs/_build/html/_static/searchtools.js new file mode 100644 index 0000000..e29b1c7 --- /dev/null +++ b/docs/_build/html/_static/searchtools.js @@ -0,0 +1,693 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +// prettier-ignore +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _escapeHTML = (text) => { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}; + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = _escapeHTML(title); + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + ` (${_escapeHTML(descr)})`; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + } else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor), + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.", + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace("${resultCount}", resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5, + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => + query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter((term) => term); // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString( + htmlString, + "text/html", + ); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { + el.remove(); + }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector( + `[role="main"] ${anchor}`, + ); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`, + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.", + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords set is from language_data.js + if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + localStorage.setItem( + "sphinx_highlight_terms", + [...highlightTerms].join(" "), + ); + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: ( + query, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if ( + title.toLowerCase().trim().includes(queryLower) + && queryLower.length >= title.length / 2 + ) { + for (const [file, id] of foundTitles) { + const score = Math.round( + (Scorer.title * queryLower.length) / title.length, + ); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round((100 * queryLower.length) / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)), + ); + + // lookup as search terms in fulltext + normalResults.push( + ...Search.performTermsSearch(searchTerms, excludedTerms), + ); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result + .slice(0, 4) + .concat([result[5]]) + .map((v) => String(v)) + .join(","); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [ + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ] = Search._parseQuery(query); + const results = Search._performSearch( + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4]; + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => objectSearchCallback(prefix, array)), + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + // find documents, if any, containing the query word in their text/title term indices + // use Object.hasOwnProperty to avoid mismatching against prototype properties + const arr = [ + { + files: terms.hasOwnProperty(word) ? terms[word] : undefined, + score: Scorer.term, + }, + { + files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, + score: Scorer.title, + }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2, + ).length; + if ( + wordList.length !== searchTerms.size + && wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file + || titleTerms[term] === file + || (terms[term] || []).includes(file) + || (titleTerms[term] || []).includes(file), + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = + top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/_build/html/_static/sphinx_highlight.js b/docs/_build/html/_static/sphinx_highlight.js new file mode 100644 index 0000000..a74e103 --- /dev/null +++ b/docs/_build/html/_static/sphinx_highlight.js @@ -0,0 +1,159 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true; + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 + && !parent.classList.contains(className) + && !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore(span, parent.insertBefore(rest, node.nextSibling)); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target), + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms"); + // Update history only if '?highlight' is present; otherwise it + // clears text fragments (not set in window.location by the browser) + if (url.searchParams.has("highlight")) { + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + } + + // get individual terms from highlight string + const terms = highlight + .toLowerCase() + .split(/\s+/) + .filter((x) => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '", + ), + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms"); + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) + return; + if ( + DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + && event.key === "Escape" + ) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/_build/html/api_reference.html b/docs/_build/html/api_reference.html new file mode 100644 index 0000000..01346aa --- /dev/null +++ b/docs/_build/html/api_reference.html @@ -0,0 +1,801 @@ + + + + + + + + Riferimento API — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Riferimento API

+

La sezione seguente usa autodoc per estrarre docstring direttamente dai +moduli Python principali del progetto.

+
+

main.py

+

Application entry point for the warehouse desktop tool.

+

This module wires together the shared async database client, the global +background event loop and the different Tk/CustomTkinter windows exposed by the +project.

+
+
+main.open_pickinglist_window(parent, db_client)
+

Open the picking list window while minimizing initial flicker.

+
+
Parameters:
+
+
+
+
+ +
+
+class main.Launcher
+

Bases: CTk

+

Main launcher window that exposes the available warehouse tools.

+
+ +
+
+

async_msssql_query.py

+

Async SQL Server access layer used by the warehouse application.

+

The module centralizes DSN creation and exposes AsyncMSSQLClient, +which lazily binds a SQLAlchemy async engine to the running event loop. The +implementation intentionally avoids pooling because the GUI schedules work on a +single shared background loop and pooled connections were a source of +cross-loop errors.

+
+
+async_msssql_query.make_mssql_dsn(*, server, database, user=None, password=None, driver='ODBC Driver 17 for SQL Server', trust_server_certificate=True, encrypt=None, extra_odbc_kv=None)
+

Build a SQLAlchemy mssql+aioodbc DSN from SQL Server parameters.

+
+
Parameters:
+
    +
  • server (str)

  • +
  • database (str)

  • +
  • user (str | None)

  • +
  • password (str | None)

  • +
  • driver (str)

  • +
  • trust_server_certificate (bool)

  • +
  • encrypt (str | None)

  • +
  • extra_odbc_kv (Dict[str, str] | None)

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+class async_msssql_query.AsyncMSSQLClient(dsn, *, echo=False, log=True)
+

Bases: object

+

Thin async query client for SQL Server.

+

The engine is created lazily on the currently running event loop and uses +sqlalchemy.pool.NullPool to avoid recycling connections across +loops or threads.

+
+
Parameters:
+
    +
  • dsn (str)

  • +
  • echo (bool)

  • +
  • log (bool)

  • +
+
+
+
+
+async dispose()
+

Dispose the engine on the loop where it was created.

+
+ +
+
+async query_json(sql, params=None, *, as_dict_rows=False)
+

Execute a query and return a JSON-friendly payload.

+
+
Parameters:
+
    +
  • sql (str) – SQL statement to execute.

  • +
  • params (Dict[str, Any] | None) – Optional named parameters bound to the statement.

  • +
  • as_dict_rows (bool) – When True returns rows as dictionaries keyed by +column name; otherwise rows are returned as lists.

  • +
+
+
Returns:
+

A dictionary containing column names, rows and elapsed execution +time in milliseconds.

+
+
Return type:
+

Dict[str, Any]

+
+
+
+ +
+
+async exec(sql, params=None, *, commit=False)
+

Execute a DML statement and return its row count.

+
+
Parameters:
+
    +
  • sql (str)

  • +
  • params (Dict[str, Any] | None)

  • +
  • commit (bool)

  • +
+
+
Return type:
+

int

+
+
+
+ +
+ +
+
+

gestione_aree_frame_async.py

+

Shared Tk/async helpers used by multiple warehouse windows.

+

The module bundles three concerns used throughout the GUI:

+
    +
  • lifecycle of the shared background asyncio loop;

  • +
  • a modal-like busy overlay shown during long-running tasks;

  • +
  • an AsyncRunner that schedules coroutines and re-enters Tk safely.

  • +
+
+
+gestione_aree_frame_async.get_global_loop()
+

Return the shared background event loop, creating it if needed.

+
+
Return type:
+

AbstractEventLoop

+
+
+
+ +
+
+gestione_aree_frame_async.stop_global_loop()
+

Stop the shared event loop and release thread references.

+
+ +
+
+class gestione_aree_frame_async.BusyOverlay(parent)
+

Bases: object

+

Semi-transparent overlay used to block interaction during async tasks.

+
+
Parameters:
+

parent (tk.Misc)

+
+
+
+
+show(message='Attendere...')
+

Display the overlay or just update its message if already visible.

+
+ +
+
+hide()
+

Dismiss the overlay and unregister resize bindings.

+
+ +
+ +
+
+class gestione_aree_frame_async.AsyncRunner(widget)
+

Bases: object

+

Run awaitables on the shared loop and callback on Tk’s main thread.

+
+
Parameters:
+

widget (tk.Misc)

+
+
+
+
+run(awaitable, on_success, on_error=None, busy=None, message='Operazione in corso...')
+

Schedule awaitable and dispatch completion callbacks in Tk.

+
+
Parameters:
+
    +
  • on_success (Callable[[Any], None])

  • +
  • on_error (Callable[[BaseException], None] | None)

  • +
  • busy (BusyOverlay | None)

  • +
  • message (str)

  • +
+
+
+
+ +
+
+close()
+

No-op kept for API compatibility with older callers.

+
+ +
+ +
+
+

layout_window.py

+

Graphical aisle layout viewer for warehouse cells and pallet occupancy.

+
+
+layout_window.pct_text(p_full, p_double=None)
+

Format occupancy percentages for the progress-bar labels.

+
+
Parameters:
+
    +
  • p_full (float)

  • +
  • p_double (float | None)

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+class layout_window.LayoutWindow(parent, db_app)
+

Bases: 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. +
  3. barcode UDC (primo, se presente)

  4. +
+
+
    +
  • Ricerca per barcode UDC con cambio automatico corsia + highlight cella

  • +
  • Statistiche: globale e corsia selezionata

  • +
  • Export XLSX

  • +
+
+
Parameters:
+

parent (tk.Widget)

+
+
+
+
+destroy()
+

Mark the window as closed and release dynamic widgets safely.

+
+ +
+ +
+
+layout_window.open_layout_window(parent, db_app)
+

Open the layout window as a singleton-like child of parent.

+
+ +
+
+

reset_corsie.py

+

Window used to inspect and empty an entire warehouse aisle.

+

The module exposes a destructive maintenance tool: it summarizes the occupancy +state of a selected aisle and, after explicit confirmation, deletes matching +rows from MagazziniPallet.

+
+
+class reset_corsie.ResetCorsieWindow(parent, db_client)
+

Bases: CTkToplevel

+

Toplevel used to inspect and clear the pallets assigned to an aisle.

+
+
+refresh()
+

Refresh both the summary counters and the occupied-cell list.

+
+ +
+ +
+
+reset_corsie.open_reset_corsie_window(parent, db_app)
+

Create, focus and return the aisle reset window.

+
+ +
+
+

view_celle_multiple.py

+

Exploration window for cells containing more than one pallet.

+
+
+class view_celle_multiple.CelleMultipleWindow(root, db_client, runner=None)
+

Bases: CTkToplevel

+

Tree-based explorer for duplicated pallet allocations.

+
+
Parameters:
+

runner (AsyncRunner | None)

+
+
+
+
+refresh_all()
+

Reload both the duplication tree and the summary percentage table.

+
+ +
+
+expand_all()
+

Expand all aisle roots and trigger lazy loading where needed.

+
+ +
+
+collapse_all()
+

Collapse all root nodes in the duplication tree.

+
+ +
+
+export_to_xlsx()
+

Export both the detailed tree and the summary table to Excel.

+
+ +
+ +
+
+view_celle_multiple.open_celle_multiple_window(root, db_client, runner=None)
+

Create, focus and return the duplicated-cells explorer.

+
+
Parameters:
+
+
+
+
+ +
+
+

search_pallets.py

+

Search window for pallets, lots and product codes across the warehouse.

+
+
+class search_pallets.SearchWindow(parent, db_app)
+

Bases: CTkToplevel

+

Window that searches pallets by barcode, lot or product code.

+
+
Parameters:
+

parent (tk.Widget)

+
+
+
+ +
+
+search_pallets.open_search_window(parent, db_app)
+

Open a singleton-like search window tied to the launcher instance.

+
+ +
+
+

gestione_pickinglist.py

+

Picking list management window.

+

The module presents a master/detail UI for packing lists, supports reservation +and unreservation through an async stored-procedure port and keeps rendering +smooth by relying on deferred updates and lightweight progress indicators.

+
+
+class gestione_pickinglist.ColSpec(title, key, width, anchor)
+

Bases: object

+

Describe one logical column rendered in a ScrollTable.

+
+
Parameters:
+
    +
  • title (str)

  • +
  • key (str)

  • +
  • width (int)

  • +
  • anchor (str)

  • +
+
+
+
+
+title: str
+
+ +
+
+key: str
+
+ +
+
+width: int
+
+ +
+
+anchor: str
+
+ +
+ +
+
+class gestione_pickinglist.ToolbarSpinner(parent)
+

Bases: object

+

Micro-animazione leggerissima per indicare attività: +mostra una label con frame: ◐ ◓ ◑ ◒ … finché è attivo.

+
+
Parameters:
+

parent (tk.Widget)

+
+
+
+
+FRAMES = ('◐', '◓', '◑', '◒')
+
+ +
+
+widget()
+

Return the label widget hosting the spinner animation.

+
+
Return type:
+

CTkLabel

+
+
+
+ +
+
+start(text='')
+

Start the animation and optionally show a short status message.

+
+
Parameters:
+

text (str)

+
+
+
+ +
+
+stop()
+

Stop the animation and clear the label text.

+
+ +
+ +
+
+class gestione_pickinglist.ScrollTable(master, columns)
+

Bases: CTkFrame

+
+
Parameters:
+

columns (List[ColSpec])

+
+
+
+
+GRID_COLOR = '#D0D5DD'
+
+ +
+
+PADX_L = 8
+
+ +
+
+PADX_R = 8
+
+ +
+
+PADY = 2
+
+ +
+
+clear_rows()
+

Remove all rendered body rows.

+
+ +
+
+add_row(values, row_index, anchors=None, checkbox_builder=None)
+

Append one row to the table body.

+
+
Parameters:
+
    +
  • values (List[str])

  • +
  • row_index (int)

  • +
  • anchors (List[str] | None)

  • +
  • checkbox_builder (Callable[[Widget], CTkCheckBox] | None)

  • +
+
+
+
+ +
+ +
+
+class gestione_pickinglist.PLRow(pl, on_check)
+

Bases: object

+

State holder for one picking list row and its selection checkbox.

+
+
Parameters:
+

pl (Dict[str, Any])

+
+
+
+
+is_checked()
+

Return whether the row is currently selected.

+
+
Return type:
+

bool

+
+
+
+ +
+
+set_checked(val)
+

Programmatically update the checkbox state.

+
+
Parameters:
+

val (bool)

+
+
+
+ +
+
+build_checkbox(parent)
+

Create the checkbox widget bound to this row model.

+
+
Return type:
+

CTkCheckBox

+
+
+
+ +
+ +
+
+class gestione_pickinglist.GestionePickingListFrame(master, *, db_client=None, conn_str=None)
+

Bases: CTkFrame

+
+
+rows_models: list[PLRow]
+
+ +
+
+on_row_checked(model, is_checked)
+

Handle row selection changes and refresh the detail section.

+
+
Parameters:
+
    +
  • model (PLRow)

  • +
  • is_checked (bool)

  • +
+
+
+
+ +
+
+reload_from_db(first=False)
+

Load or reload the picking list summary table from the database.

+
+
Parameters:
+

first (bool)

+
+
+
+ +
+
+on_prenota()
+

Reserve the selected picking list.

+
+ +
+
+on_sprenota()
+

Unreserve the selected picking list.

+
+ +
+
+on_export()
+

Placeholder for a future export implementation.

+
+ +
+ +
+
+gestione_pickinglist.create_frame(parent, *, db_client=None, conn_str=None)
+

Factory used by the launcher to build the picking list frame.

+
+
Return type:
+

GestionePickingListFrame

+
+
+
+ +
+
+

prenota_sprenota_sql.py

+

Async port of the packing list reservation stored procedure.

+
+
+class prenota_sprenota_sql.SPResult(rc=0, message='', id_result=None)
+

Bases: object

+

Container returned by the async stored-procedure port.

+
+
Parameters:
+
    +
  • rc (int)

  • +
  • message (str | None)

  • +
  • id_result (int | None)

  • +
+
+
+
+
+rc: int = 0
+
+ +
+
+message: str | None = ''
+
+ +
+
+id_result: int | None = None
+
+ +
+ +
+
+async prenota_sprenota_sql.sp_xExePackingListPallet_async(db, IDOperatore, Documento)
+

Toggle the reservation state of all cells belonging to a packing list.

+

The implementation mirrors the original SQL stored procedure while using +the shared async DB client already managed by the application.

+
+
Parameters:
+
    +
  • IDOperatore (int)

  • +
  • Documento (str)

  • +
+
+
Return type:
+

SPResult

+
+
+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/architecture.html b/docs/_build/html/architecture.html new file mode 100644 index 0000000..8280bd2 --- /dev/null +++ b/docs/_build/html/architecture.html @@ -0,0 +1,600 @@ + + + + + + + + Architettura Complessiva — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Architettura Complessiva

+

Questa pagina collega i moduli principali del progetto in una vista unica, +partendo dal launcher fino ai moduli GUI e al livello infrastrutturale async/DB.

+
+

Vista architetturale

+
+        flowchart TD
+    Main["main.py"] --> Launcher["Launcher"]
+    Main --> Loop["async_loop_singleton.get_global_loop()"]
+    Main --> DB["AsyncMSSQLClient"]
+
+    Launcher --> Reset["reset_corsie.py"]
+    Launcher --> Layout["layout_window.py"]
+    Launcher --> Ghost["view_celle_multiple.py"]
+    Launcher --> Search["search_pallets.py"]
+    Launcher --> Picking["gestione_pickinglist.py"]
+
+    Reset --> Runner["gestione_aree_frame_async.AsyncRunner"]
+    Layout --> Runner
+    Ghost --> Runner
+    Search --> Runner
+    Picking --> Runner
+
+    Runner --> Loop
+    Runner --> DB
+    Picking --> SP["prenota_sprenota_sql.py"]
+    SP --> DB
+    DB --> SQL["SQL Server / Mediseawall"]
+    
+
+

Flusso applicativo generale

+
+        flowchart LR
+    User["Utente"] --> MainWin["Launcher"]
+    MainWin --> Module["Finestra modulo"]
+    Module --> AsyncReq["AsyncRunner.run(...)"]
+    AsyncReq --> DbClient["AsyncMSSQLClient"]
+    DbClient --> SqlServer["Database SQL Server"]
+    SqlServer --> Callback["Callback _ok/_err"]
+    Callback --> Module
+    
+
+

Osservazioni

+
    +
  • main.py centralizza il loop asincrono e il client database condiviso.

  • +
  • I moduli GUI si concentrano sulla UI e delegano query e task lunghi a AsyncRunner.

  • +
  • gestione_pickinglist.py è l’unico modulo che passa anche da prenota_sprenota_sql.py per la logica di prenotazione.

  • +
  • La cartella docs/flows/ contiene la vista dettagliata modulo per modulo.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/README.html b/docs/_build/html/flows/README.html new file mode 100644 index 0000000..dcc7c98 --- /dev/null +++ b/docs/_build/html/flows/README.html @@ -0,0 +1,154 @@ + + + + + + + + Flow Diagrams — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Flow Diagrams

+

Questa cartella contiene schemi di flusso e schemi di chiamata dei moduli +principali avviati da main.py.

+

I diagrammi sono scritti in Mermaid, quindi possono essere:

+
    +
  • letti direttamente nei file Markdown;

  • +
  • renderizzati da molti editor Git/Markdown;

  • +
  • inclusi in una futura documentazione Sphinx o MkDocs.

  • +
+
+

Indice

+ +
+
+

Convenzioni

+
    +
  • I diagrammi descrivono il flusso applicativo ad alto livello.

  • +
  • Non rappresentano ogni singola riga di codice.

  • +
  • I nodi AsyncRunner e query_json evidenziano i passaggi asincroni più +importanti tra interfaccia e database.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/async_db_flow.html b/docs/_build/html/flows/async_db_flow.html new file mode 100644 index 0000000..2aba7a6 --- /dev/null +++ b/docs/_build/html/flows/async_db_flow.html @@ -0,0 +1,599 @@ + + + + + + + + Infrastruttura Async / DB — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Infrastruttura Async / DB

+
+

Scopo

+

Questo diagramma descrive il flusso comune usato da tutti i moduli GUI quando +eseguono una query sul database.

+
+
+

Flusso trasversale

+
+        flowchart TD
+    A["Evento UI (click / selezione / ricerca)"] --> B["Metodo finestra"]
+    B --> C["AsyncRunner.run(awaitable)"]
+    C --> D["Coroutines sul loop globale"]
+    D --> E["AsyncMSSQLClient.query_json() / exec()"]
+    E --> F["SQL Server"]
+    F --> G["Risultato query"]
+    G --> H["Future completata"]
+    H --> I["Callback _ok / _err su thread Tk"]
+    I --> J["Aggiornamento widget"]
+    
+
+

Relazioni principali

+
+        flowchart LR
+    Main["main.py"] --> Loop["get_global_loop()"]
+    Main --> DB["AsyncMSSQLClient"]
+    Windows["Moduli GUI"] --> Runner["AsyncRunner"]
+    Runner --> Loop
+    Runner --> DB
+    DB --> SQL["SQL Server Mediseawall"]
+    
+
+

Note

+
    +
  • Il loop asincrono è condiviso tra tutte le finestre.

  • +
  • Il client DB è condiviso e creato una sola volta nel launcher.

  • +
  • I callback che aggiornano la UI rientrano sempre sul thread Tk.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/async_loop_singleton_flow.html b/docs/_build/html/flows/async_loop_singleton_flow.html new file mode 100644 index 0000000..ea1e8b5 --- /dev/null +++ b/docs/_build/html/flows/async_loop_singleton_flow.html @@ -0,0 +1,597 @@ + + + + + + + + async_loop_singleton.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

async_loop_singleton.py

+
+

Scopo

+

Questo modulo mantiene un loop asyncio globale e condiviso, eseguito su un +thread dedicato.

+
+
+

Flusso

+
+        flowchart TD
+    A["Chiamata a get_global_loop()"] --> B{"Loop gia presente?"}
+    B -- Si --> C["Ritorna loop esistente"]
+    B -- No --> D["Crea Event ready"]
+    D --> E["Avvia thread daemon"]
+    E --> F["_run()"]
+    F --> G["new_event_loop()"]
+    G --> H["set_event_loop(loop)"]
+    H --> I["ready.set()"]
+    I --> J["loop.run_forever()"]
+    J --> K["Ritorna loop al chiamante"]
+    
+
+

Chiusura

+
+        flowchart TD
+    A["stop_global_loop()"] --> B{"Loop attivo?"}
+    B -- No --> C["Nessuna azione"]
+    B -- Si --> D["call_soon_threadsafe(loop.stop)"]
+    D --> E["join del thread"]
+    E --> F["Azzera riferimenti globali"]
+    
+
+

Note

+
    +
  • E un helper minimale usato da main.py.

  • +
  • Il modulo esiste separato da gestione_aree_frame_async.py, ma concettualmente +svolge lo stesso ruolo di gestione del loop condiviso.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/async_msssql_query_flow.html b/docs/_build/html/flows/async_msssql_query_flow.html new file mode 100644 index 0000000..f54ad38 --- /dev/null +++ b/docs/_build/html/flows/async_msssql_query_flow.html @@ -0,0 +1,601 @@ + + + + + + + + async_msssql_query.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

async_msssql_query.py

+
+

Scopo

+

Questo modulo centralizza la costruzione del DSN SQL Server e l’accesso +asincrono al database tramite AsyncMSSQLClient.

+
+
+

Flusso di utilizzo

+
+        flowchart TD
+    A["main.py o modulo chiamante"] --> B["make_mssql_dsn(...)"]
+    B --> C["Crea stringa mssql+aioodbc"]
+    C --> D["AsyncMSSQLClient(dsn)"]
+    D --> E["query_json(...) o exec(...)"]
+    E --> F["_ensure_engine()"]
+    F --> G{"Engine gia creato?"}
+    G -- No --> H["create_async_engine(..., NullPool, loop corrente)"]
+    G -- Si --> I["Riusa engine esistente"]
+    H --> J["execute(text(sql), params)"]
+    I --> J
+    J --> K["Normalizza rows/columns"]
+    K --> L["Ritorna payload JSON-friendly"]
+    
+
+

Schema di chiamata

+
+        flowchart LR
+    DSN["make_mssql_dsn"] --> Client["AsyncMSSQLClient.__init__"]
+    Client --> Ensure["_ensure_engine"]
+    Ensure --> Query["query_json"]
+    Ensure --> Exec["exec"]
+    Client --> Dispose["dispose"]
+    
+
+

Note

+
    +
  • NullPool evita problemi di riuso connessioni tra loop diversi.

  • +
  • L’engine viene creato solo al primo utilizzo reale.

  • +
  • query_json() restituisce un formato gia pronto per le callback GUI.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/gestione_aree_frame_async_flow.html b/docs/_build/html/flows/gestione_aree_frame_async_flow.html new file mode 100644 index 0000000..0d446e0 --- /dev/null +++ b/docs/_build/html/flows/gestione_aree_frame_async_flow.html @@ -0,0 +1,606 @@ + + + + + + + + gestione_aree_frame_async.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

gestione_aree_frame_async.py

+
+

Scopo

+

Questo modulo fornisce l’infrastruttura async usata dalle finestre GUI:

+
    +
  • loop asincrono globale;

  • +
  • overlay di attesa;

  • +
  • runner che collega coroutine e callback Tk.

  • +
+
+
+

Flusso infrastrutturale

+
+        flowchart TD
+    A["Metodo finestra GUI"] --> B["AsyncRunner.run(awaitable)"]
+    B --> C{"busy overlay richiesto?"}
+    C -- Si --> D["BusyOverlay.show()"]
+    C -- No --> E["Salta overlay"]
+    D --> F["run_coroutine_threadsafe(awaitable, loop globale)"]
+    E --> F
+    F --> G["Polling del Future"]
+    G --> H{"Future completato?"}
+    H -- No --> G
+    H -- Si --> I{"Successo o errore?"}
+    I -- Successo --> J["widget.after(..., on_success)"]
+    I -- Errore --> K["widget.after(..., on_error)"]
+    J --> L["BusyOverlay.hide()"]
+    K --> L
+    
+
+

Schema di componenti

+
+        flowchart LR
+    Holder["_LoopHolder"] --> Loop["get_global_loop"]
+    Loop --> Runner["AsyncRunner"]
+    Overlay["BusyOverlay"] --> Runner
+    Runner --> GUI["Moduli GUI"]
+    
+
+

Note

+
    +
  • Il modulo fa da ponte tra thread Tk e thread del loop asincrono.

  • +
  • BusyOverlay e riusato da piu finestre, quindi e un componente condiviso.

  • +
  • AsyncRunner evita che i moduli GUI gestiscano direttamente i Future.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/gestione_pickinglist_flow.html b/docs/_build/html/flows/gestione_pickinglist_flow.html new file mode 100644 index 0000000..f86df62 --- /dev/null +++ b/docs/_build/html/flows/gestione_pickinglist_flow.html @@ -0,0 +1,628 @@ + + + + + + + + gestione_pickinglist.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

gestione_pickinglist.py

+
+

Scopo

+

Questo modulo gestisce la vista master/detail delle picking list e permette di:

+
    +
  • caricare l’elenco dei documenti;

  • +
  • vedere il dettaglio UDC della riga selezionata;

  • +
  • prenotare e s-prenotare una picking list;

  • +
  • mantenere una UI fluida con spinner e refresh differiti.

  • +
+
+
+

Flusso di apertura

+
+        flowchart TD
+    A["open_pickinglist_window() da main.py"] --> B["create_pickinglist_frame()"]
+    B --> C["GestionePickingListFrame.__init__()"]
+    C --> D["_build_layout()"]
+    D --> E["after_idle(_first_show)"]
+    E --> F["reload_from_db(first=True)"]
+    F --> G["query_json SQL_PL"]
+    G --> H["_refresh_mid_rows()"]
+    H --> I["Render tabella master"]
+    
+
+

Flusso master/detail

+
+        flowchart TD
+    A["Utente seleziona checkbox riga"] --> B["on_row_checked()"]
+    B --> C["Deseleziona altre righe"]
+    C --> D["Salva detail_doc"]
+    D --> E["query_json SQL_PL_DETAILS"]
+    E --> F["_refresh_details()"]
+    F --> G["Render tabella dettaglio"]
+    
+
+

Prenotazione / s-prenotazione

+
+        flowchart TD
+    A["Click Prenota o S-prenota"] --> B["Verifica riga selezionata"]
+    B --> C["Determina documento e stato atteso"]
+    C --> D["Chiama sp_xExePackingListPallet_async()"]
+    D --> E["Aggiorna Celle e LogPackingList sul DB"]
+    E --> F["SPResult"]
+    F --> G{"rc == 0?"}
+    G -- Si --> H["_recolor_row_by_documento()"]
+    G -- No --> I["Messaggio di errore"]
+    
+
+

Schema di chiamata

+
+        flowchart LR
+    Init["__init__"] --> Build["_build_layout"]
+    Init --> First["_first_show"]
+    First --> Reload["reload_from_db"]
+    Reload --> Mid["_refresh_mid_rows"]
+    Check["on_row_checked"] --> Details["_refresh_details"]
+    Pren["on_prenota"] --> SP["sp_xExePackingListPallet_async"]
+    Spren["on_sprenota"] --> SP
+    
+
+

Note

+
    +
  • Il modulo usa AsyncRunner, BusyOverlay e ToolbarSpinner.

  • +
  • Il caricamento iniziale è differito con after_idle per ridurre lo sfarfallio.

  • +
  • La riga selezionata viene tenuta separata dal dettaglio tramite detail_doc.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/index.html b/docs/_build/html/flows/index.html new file mode 100644 index 0000000..697666a --- /dev/null +++ b/docs/_build/html/flows/index.html @@ -0,0 +1,140 @@ + + + + + + + + Flow Diagrams — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Flow Diagrams

+

Questa sezione raccoglie i diagrammi Mermaid dei moduli applicativi e +infrastrutturali.

+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/layout_window_flow.html b/docs/_build/html/flows/layout_window_flow.html new file mode 100644 index 0000000..7b4a985 --- /dev/null +++ b/docs/_build/html/flows/layout_window_flow.html @@ -0,0 +1,620 @@ + + + + + + + + layout_window.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

layout_window.py

+
+

Scopo

+

Questo modulo visualizza il layout delle corsie come matrice di celle, mostra +lo stato di occupazione, consente di cercare una UDC e permette l’export della +matrice.

+
+
+

Flusso operativo

+
+        flowchart TD
+    A["open_layout_window()"] --> B["Crea o riporta in primo piano LayoutWindow"]
+    B --> C["LayoutWindow.__init__()"]
+    C --> D["Costruisce toolbar, host matrice, statistiche"]
+    D --> E["_load_corsie()"]
+    E --> F["AsyncRunner.run(query_json SQL corsie)"]
+    F --> G["_on_select() sulla corsia iniziale"]
+    G --> H["_load_matrix(corsia)"]
+    H --> I["AsyncRunner.run(query_json SQL matrice)"]
+    I --> J["_rebuild_matrix()"]
+    J --> K["_refresh_stats()"]
+    
+
+

Ricerca UDC

+
+        flowchart TD
+    A["Utente inserisce barcode"] --> B["_search_udc()"]
+    B --> C["query_json ricerca pallet -> corsia/colonna/fila"]
+    C --> D{"UDC trovata?"}
+    D -- No --> E["Messaggio informativo"]
+    D -- Si --> F["Seleziona corsia in listbox"]
+    F --> G["_load_matrix(corsia)"]
+    G --> H["_rebuild_matrix()"]
+    H --> I["_highlight_cell_by_labels()"]
+    
+
+

Schema di chiamata essenziale

+
+        flowchart LR
+    Init["__init__"] --> Top["_build_top"]
+    Init --> Host["_build_matrix_host"]
+    Init --> Stats["_build_stats"]
+    Init --> LoadCorsie["_load_corsie"]
+    LoadCorsie --> Select["_on_select"]
+    Select --> LoadMatrix["_load_matrix"]
+    LoadMatrix --> Rebuild["_rebuild_matrix"]
+    Rebuild --> RefreshStats["_refresh_stats"]
+    Search["_search_udc"] --> LoadMatrix
+    Export["_export_xlsx"] --> MatrixState["matrix_state / fila_txt / col_txt / udc1"]
+    
+
+

Note

+
    +
  • Il modulo usa un token _req_counter per evitare che risposte async vecchie +aggiornino la UI fuori ordine.

  • +
  • La statistica globale viene ricalcolata da query SQL, mentre quella della +corsia corrente usa la matrice già caricata in memoria.

  • +
  • destroy() marca la finestra come non più attiva per evitare callback tardive.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/main_flow.html b/docs/_build/html/flows/main_flow.html new file mode 100644 index 0000000..7a2db00 --- /dev/null +++ b/docs/_build/html/flows/main_flow.html @@ -0,0 +1,605 @@ + + + + + + + + main.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

main.py

+
+

Scopo

+

main.py è il punto di ingresso dell’applicazione desktop. Inizializza il loop +asincrono condiviso, crea il client database condiviso e costruisce il launcher +con i pulsanti che aprono i moduli operativi.

+
+
+

Flusso principale

+
+        flowchart TD
+    A["Avvio di main.py"] --> B["Configura policy asyncio su Windows"]
+    B --> C["Ottiene loop globale con get_global_loop()"]
+    C --> D["Imposta il loop come default"]
+    D --> E["Costruisce DSN SQL Server"]
+    E --> F["Crea AsyncMSSQLClient condiviso"]
+    F --> G["Istanzia Launcher"]
+    G --> H["Mostra finestra principale"]
+    H --> I{"Click su un pulsante"}
+    I --> J["open_reset_corsie_window()"]
+    I --> K["open_layout_window()"]
+    I --> L["open_celle_multiple_window()"]
+    I --> M["open_search_window()"]
+    I --> N["open_pickinglist_window()"]
+    
+
+

Schema di chiamata

+
+        flowchart LR
+    Launcher["Launcher.__init__"] --> Reset["open_reset_corsie_window"]
+    Launcher --> Layout["open_layout_window"]
+    Launcher --> Ghost["open_celle_multiple_window"]
+    Launcher --> Search["open_search_window"]
+    Launcher --> Pick["open_pickinglist_window"]
+    Pick --> PickFactory["create_pickinglist_frame"]
+    
+
+

Note

+
    +
  • db_app viene creato una sola volta e poi passato a tutte le finestre.

  • +
  • Alla chiusura del launcher viene chiamato db_app.dispose() sul loop globale.

  • +
  • open_pickinglist_window() costruisce la finestra in modo nascosto e la rende +visibile solo a layout pronto, per ridurre lo sfarfallio iniziale.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/reset_corsie_flow.html b/docs/_build/html/flows/reset_corsie_flow.html new file mode 100644 index 0000000..a2104f9 --- /dev/null +++ b/docs/_build/html/flows/reset_corsie_flow.html @@ -0,0 +1,605 @@ + + + + + + + + reset_corsie.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

reset_corsie.py

+
+

Scopo

+

Questo modulo mostra il riepilogo di una corsia e permette, dopo doppia +conferma, di cancellare i record di MagazziniPallet collegati a quella corsia.

+
+
+

Flusso operativo

+
+        flowchart TD
+    A["open_reset_corsie_window()"] --> B["ResetCorsieWindow.__init__()"]
+    B --> C["_build_ui()"]
+    C --> D["_load_corsie()"]
+    D --> E["query_json SQL_CORSIE"]
+    E --> F["Seleziona corsia iniziale"]
+    F --> G["refresh()"]
+    G --> H["query_json SQL_RIEPILOGO"]
+    G --> I["query_json SQL_DETTAGLIO"]
+    H --> J["Aggiorna contatori"]
+    I --> K["Aggiorna tree celle occupate"]
+    
+
+

Flusso distruttivo

+
+        flowchart TD
+    A["Click su Svuota corsia"] --> B["_ask_reset()"]
+    B --> C["query_json SQL_COUNT_DELETE"]
+    C --> D{"Record da cancellare > 0?"}
+    D -- No --> E["Messaggio: niente da rimuovere"]
+    D -- Si --> F["Richiesta conferma testuale"]
+    F --> G{"Testo corretto?"}
+    G -- No --> H["Annulla operazione"]
+    G -- Si --> I["_do_reset(corsia)"]
+    I --> J["query_json SQL_DELETE"]
+    J --> K["Messaggio completato"]
+    K --> L["refresh()"]
+    
+
+

Note

+
    +
  • È il modulo più delicato lato operazioni, perché esegue DELETE.

  • +
  • La finestra separa chiaramente fase di ispezione e fase distruttiva.

  • +
  • Tutte le query passano dal client condiviso tramite AsyncRunner.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/search_pallets_flow.html b/docs/_build/html/flows/search_pallets_flow.html new file mode 100644 index 0000000..13e65a4 --- /dev/null +++ b/docs/_build/html/flows/search_pallets_flow.html @@ -0,0 +1,603 @@ + + + + + + + + search_pallets.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

search_pallets.py

+
+

Scopo

+

Questo modulo consente di cercare pallet/UDC, lotti e codici prodotto su tutto +il magazzino e di esportare i risultati.

+
+
+

Flusso operativo

+
+        flowchart TD
+    A["open_search_window()"] --> B["SearchWindow.__init__()"]
+    B --> C["_build_ui()"]
+    C --> D["Utente compila filtri"]
+    D --> E["_do_search()"]
+    E --> F{"Filtri vuoti?"}
+    F -- Si --> G["Richiesta conferma ricerca globale"]
+    F -- No --> H["Prepara parametri SQL"]
+    G --> H
+    H --> I["AsyncRunner.run(query_json SQL_SEARCH)"]
+    I --> J["_ok()"]
+    J --> K["Popola Treeview"]
+    K --> L["Eventuale reset campi"]
+    
+
+

Ordinamento ed export

+
+        flowchart TD
+    A["Doppio click su header"] --> B["_on_heading_double_click()"]
+    B --> C["_sort_by_column()"]
+    C --> D["Riordina righe del Treeview"]
+
+    E["Click Export XLSX"] --> F["_export_xlsx()"]
+    F --> G["Legge righe visibili"]
+    G --> H["Scrive workbook Excel"]
+    
+
+

Note

+
    +
  • Il modulo usa Treeview come backend principale.

  • +
  • Le ricerche possono essere molto ampie: per questo, senza filtri, viene chiesta conferma.

  • +
  • IDCella = 9999 viene evidenziata con uno stile dedicato.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/flows/view_celle_multiple_flow.html b/docs/_build/html/flows/view_celle_multiple_flow.html new file mode 100644 index 0000000..055dd4b --- /dev/null +++ b/docs/_build/html/flows/view_celle_multiple_flow.html @@ -0,0 +1,618 @@ + + + + + + + + view_celle_multiple.py — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

view_celle_multiple.py

+
+

Scopo

+

Questo modulo esplora le celle che contengono più pallet del previsto, +organizzando i risultati in un albero:

+
    +
  • corsia

  • +
  • cella duplicata

  • +
  • pallet contenuti nella cella

  • +
+
+
+

Flusso operativo

+
+        flowchart TD
+    A["open_celle_multiple_window()"] --> B["CelleMultipleWindow.__init__()"]
+    B --> C["_build_layout()"]
+    C --> D["_bind_events()"]
+    D --> E["refresh_all()"]
+    E --> F["_load_corsie()"]
+    E --> G["_load_riepilogo()"]
+    F --> H["query_json SQL_CORSIE"]
+    G --> I["query_json SQL_RIEPILOGO_PERCENTUALI"]
+    H --> J["_fill_corsie()"]
+    I --> K["_fill_riepilogo()"]
+    
+
+

Lazy loading dell’albero

+
+        flowchart TD
+    A["Espansione nodo tree"] --> B["_on_open_node()"]
+    B --> C{"Nodo corsia o nodo cella?"}
+    C -- Corsia --> D["_load_celle_for_corsia()"]
+    D --> E["query_json SQL_CELLE_DUP_PER_CORSIA"]
+    E --> F["_fill_celle()"]
+    C -- Cella --> G["_load_pallet_for_cella()"]
+    G --> H["query_json SQL_PALLET_IN_CELLA"]
+    H --> I["_fill_pallet()"]
+    
+
+

Schema di chiamata

+
+        flowchart LR
+    Refresh["refresh_all"] --> Corsie["_load_corsie"]
+    Refresh --> Riep["_load_riepilogo"]
+    Open["_on_open_node"] --> LoadCelle["_load_celle_for_corsia"]
+    Open --> LoadPallet["_load_pallet_for_cella"]
+    Export["export_to_xlsx"] --> Tree["tree dati dettaglio"]
+    Export --> Sum["sum_tbl riepilogo"]
+    
+
+

Note

+
    +
  • L’albero è caricato a richiesta, non tutto in una sola query.

  • +
  • Questo riduce il costo iniziale e rende il modulo più scalabile.

  • +
  • L’export legge sia il dettaglio dell’albero sia la tabella di riepilogo.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html new file mode 100644 index 0000000..e0ceed1 --- /dev/null +++ b/docs/_build/html/genindex.html @@ -0,0 +1,923 @@ + + + + + + + Index — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | K + | L + | M + | O + | P + | Q + | R + | S + | T + | V + | W + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + +
+ +

G

+ + + +
    +
  • + gestione_aree_frame_async + +
  • +
  • + gestione_pickinglist + +
  • +
+ +

H

+ + +
+ +

I

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

V

+ + +
    +
  • + view_celle_multiple + +
  • +
+ +

W

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html new file mode 100644 index 0000000..01beb7d --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,151 @@ + + + + + + + + Warehouse Documentation — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv new file mode 100644 index 0000000..bfb61ac Binary files /dev/null and b/docs/_build/html/objects.inv differ diff --git a/docs/_build/html/py-modindex.html b/docs/_build/html/py-modindex.html new file mode 100644 index 0000000..d075e28 --- /dev/null +++ b/docs/_build/html/py-modindex.html @@ -0,0 +1,627 @@ + + + + + + + Python Module Index — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ a | + g | + l | + m | + p | + r | + s | + v +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ a
+ async_msssql_query +
 
+ g
+ gestione_aree_frame_async +
+ gestione_pickinglist +
 
+ l
+ layout_window +
 
+ m
+ main +
 
+ p
+ prenota_sprenota_sql +
 
+ r
+ reset_corsie +
 
+ s
+ search_pallets +
 
+ v
+ view_celle_multiple +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html new file mode 100644 index 0000000..4f9d3f4 --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,559 @@ + + + + + + + Search — warehouse 0.0.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js new file mode 100644 index 0000000..ce30c0c --- /dev/null +++ b/docs/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles":{"Architettura Complessiva":[[1,null]],"Chiusura":[[4,"chiusura"]],"Contenuti":[[14,null]],"Convenzioni":[[2,"convenzioni"]],"Flow Diagrams":[[2,null],[8,null]],"Flusso":[[4,"flusso"]],"Flusso applicativo generale":[[1,"flusso-applicativo-generale"]],"Flusso di apertura":[[7,"flusso-di-apertura"]],"Flusso di utilizzo":[[5,"flusso-di-utilizzo"]],"Flusso distruttivo":[[11,"flusso-distruttivo"]],"Flusso infrastrutturale":[[6,"flusso-infrastrutturale"]],"Flusso master/detail":[[7,"flusso-master-detail"]],"Flusso operativo":[[9,"flusso-operativo"],[11,"flusso-operativo"],[12,"flusso-operativo"],[13,"flusso-operativo"]],"Flusso principale":[[10,"flusso-principale"]],"Flusso trasversale":[[3,"flusso-trasversale"]],"Indice":[[2,"indice"]],"Infrastruttura Async / DB":[[3,null]],"Lazy loading dell\u2019albero":[[13,"lazy-loading-dell-albero"]],"Note":[[3,"note"],[4,"note"],[5,"note"],[6,"note"],[7,"note"],[9,"note"],[10,"note"],[11,"note"],[12,"note"],[13,"note"]],"Ordinamento ed export":[[12,"ordinamento-ed-export"]],"Osservazioni":[[1,"osservazioni"]],"Prenotazione / s-prenotazione":[[7,"prenotazione-s-prenotazione"]],"Relazioni principali":[[3,"relazioni-principali"]],"Ricerca UDC":[[9,"ricerca-udc"]],"Riferimento API":[[0,null]],"Schema di chiamata":[[5,"schema-di-chiamata"],[7,"schema-di-chiamata"],[10,"schema-di-chiamata"],[13,"schema-di-chiamata"]],"Schema di chiamata essenziale":[[9,"schema-di-chiamata-essenziale"]],"Schema di componenti":[[6,"schema-di-componenti"]],"Scopo":[[3,"scopo"],[4,"scopo"],[5,"scopo"],[6,"scopo"],[7,"scopo"],[9,"scopo"],[10,"scopo"],[11,"scopo"],[12,"scopo"],[13,"scopo"]],"Vista architetturale":[[1,"vista-architetturale"]],"Warehouse Documentation":[[14,null]],"async_loop_singleton.py":[[4,null]],"async_msssql_query.py":[[0,"module-async_msssql_query"],[5,null]],"gestione_aree_frame_async.py":[[0,"module-gestione_aree_frame_async"],[6,null]],"gestione_pickinglist.py":[[0,"module-gestione_pickinglist"],[7,null]],"layout_window.py":[[0,"module-layout_window"],[9,null]],"main.py":[[0,"module-main"],[10,null]],"prenota_sprenota_sql.py":[[0,"module-prenota_sprenota_sql"]],"reset_corsie.py":[[0,"module-reset_corsie"],[11,null]],"search_pallets.py":[[0,"module-search_pallets"],[12,null]],"view_celle_multiple.py":[[0,"module-view_celle_multiple"],[13,null]]},"docnames":["api_reference","architecture","flows/README","flows/async_db_flow","flows/async_loop_singleton_flow","flows/async_msssql_query_flow","flows/gestione_aree_frame_async_flow","flows/gestione_pickinglist_flow","flows/index","flows/layout_window_flow","flows/main_flow","flows/reset_corsie_flow","flows/search_pallets_flow","flows/view_celle_multiple_flow","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["api_reference.rst","architecture.md","flows\\README.md","flows\\async_db_flow.md","flows\\async_loop_singleton_flow.md","flows\\async_msssql_query_flow.md","flows\\gestione_aree_frame_async_flow.md","flows\\gestione_pickinglist_flow.md","flows\\index.rst","flows\\layout_window_flow.md","flows\\main_flow.md","flows\\reset_corsie_flow.md","flows\\search_pallets_flow.md","flows\\view_celle_multiple_flow.md","index.rst"],"indexentries":{"add_row() (gestione_pickinglist.scrolltable method)":[[0,"gestione_pickinglist.ScrollTable.add_row",false]],"anchor (gestione_pickinglist.colspec attribute)":[[0,"gestione_pickinglist.ColSpec.anchor",false]],"async_msssql_query":[[0,"module-async_msssql_query",false]],"asyncmssqlclient (class in async_msssql_query)":[[0,"async_msssql_query.AsyncMSSQLClient",false]],"asyncrunner (class in gestione_aree_frame_async)":[[0,"gestione_aree_frame_async.AsyncRunner",false]],"build_checkbox() (gestione_pickinglist.plrow method)":[[0,"gestione_pickinglist.PLRow.build_checkbox",false]],"busyoverlay (class in gestione_aree_frame_async)":[[0,"gestione_aree_frame_async.BusyOverlay",false]],"cellemultiplewindow (class in view_celle_multiple)":[[0,"view_celle_multiple.CelleMultipleWindow",false]],"clear_rows() (gestione_pickinglist.scrolltable method)":[[0,"gestione_pickinglist.ScrollTable.clear_rows",false]],"close() (gestione_aree_frame_async.asyncrunner method)":[[0,"gestione_aree_frame_async.AsyncRunner.close",false]],"collapse_all() (view_celle_multiple.cellemultiplewindow method)":[[0,"view_celle_multiple.CelleMultipleWindow.collapse_all",false]],"colspec (class in gestione_pickinglist)":[[0,"gestione_pickinglist.ColSpec",false]],"create_frame() (in module gestione_pickinglist)":[[0,"gestione_pickinglist.create_frame",false]],"destroy() (layout_window.layoutwindow method)":[[0,"layout_window.LayoutWindow.destroy",false]],"dispose() (async_msssql_query.asyncmssqlclient method)":[[0,"async_msssql_query.AsyncMSSQLClient.dispose",false]],"exec() (async_msssql_query.asyncmssqlclient method)":[[0,"async_msssql_query.AsyncMSSQLClient.exec",false]],"expand_all() (view_celle_multiple.cellemultiplewindow method)":[[0,"view_celle_multiple.CelleMultipleWindow.expand_all",false]],"export_to_xlsx() (view_celle_multiple.cellemultiplewindow method)":[[0,"view_celle_multiple.CelleMultipleWindow.export_to_xlsx",false]],"frames (gestione_pickinglist.toolbarspinner attribute)":[[0,"gestione_pickinglist.ToolbarSpinner.FRAMES",false]],"gestione_aree_frame_async":[[0,"module-gestione_aree_frame_async",false]],"gestione_pickinglist":[[0,"module-gestione_pickinglist",false]],"gestionepickinglistframe (class in gestione_pickinglist)":[[0,"gestione_pickinglist.GestionePickingListFrame",false]],"get_global_loop() (in module gestione_aree_frame_async)":[[0,"gestione_aree_frame_async.get_global_loop",false]],"grid_color (gestione_pickinglist.scrolltable attribute)":[[0,"gestione_pickinglist.ScrollTable.GRID_COLOR",false]],"hide() (gestione_aree_frame_async.busyoverlay method)":[[0,"gestione_aree_frame_async.BusyOverlay.hide",false]],"id_result (prenota_sprenota_sql.spresult attribute)":[[0,"prenota_sprenota_sql.SPResult.id_result",false]],"is_checked() (gestione_pickinglist.plrow method)":[[0,"gestione_pickinglist.PLRow.is_checked",false]],"key (gestione_pickinglist.colspec attribute)":[[0,"gestione_pickinglist.ColSpec.key",false]],"launcher (class in main)":[[0,"main.Launcher",false]],"layout_window":[[0,"module-layout_window",false]],"layoutwindow (class in layout_window)":[[0,"layout_window.LayoutWindow",false]],"main":[[0,"module-main",false]],"make_mssql_dsn() (in module async_msssql_query)":[[0,"async_msssql_query.make_mssql_dsn",false]],"message (prenota_sprenota_sql.spresult attribute)":[[0,"prenota_sprenota_sql.SPResult.message",false]],"module":[[0,"module-async_msssql_query",false],[0,"module-gestione_aree_frame_async",false],[0,"module-gestione_pickinglist",false],[0,"module-layout_window",false],[0,"module-main",false],[0,"module-prenota_sprenota_sql",false],[0,"module-reset_corsie",false],[0,"module-search_pallets",false],[0,"module-view_celle_multiple",false]],"on_export() (gestione_pickinglist.gestionepickinglistframe method)":[[0,"gestione_pickinglist.GestionePickingListFrame.on_export",false]],"on_prenota() (gestione_pickinglist.gestionepickinglistframe method)":[[0,"gestione_pickinglist.GestionePickingListFrame.on_prenota",false]],"on_row_checked() (gestione_pickinglist.gestionepickinglistframe method)":[[0,"gestione_pickinglist.GestionePickingListFrame.on_row_checked",false]],"on_sprenota() (gestione_pickinglist.gestionepickinglistframe method)":[[0,"gestione_pickinglist.GestionePickingListFrame.on_sprenota",false]],"open_celle_multiple_window() (in module view_celle_multiple)":[[0,"view_celle_multiple.open_celle_multiple_window",false]],"open_layout_window() (in module layout_window)":[[0,"layout_window.open_layout_window",false]],"open_pickinglist_window() (in module main)":[[0,"main.open_pickinglist_window",false]],"open_reset_corsie_window() (in module reset_corsie)":[[0,"reset_corsie.open_reset_corsie_window",false]],"open_search_window() (in module search_pallets)":[[0,"search_pallets.open_search_window",false]],"padx_l (gestione_pickinglist.scrolltable attribute)":[[0,"gestione_pickinglist.ScrollTable.PADX_L",false]],"padx_r (gestione_pickinglist.scrolltable attribute)":[[0,"gestione_pickinglist.ScrollTable.PADX_R",false]],"pady (gestione_pickinglist.scrolltable attribute)":[[0,"gestione_pickinglist.ScrollTable.PADY",false]],"pct_text() (in module layout_window)":[[0,"layout_window.pct_text",false]],"plrow (class in gestione_pickinglist)":[[0,"gestione_pickinglist.PLRow",false]],"prenota_sprenota_sql":[[0,"module-prenota_sprenota_sql",false]],"query_json() (async_msssql_query.asyncmssqlclient method)":[[0,"async_msssql_query.AsyncMSSQLClient.query_json",false]],"rc (prenota_sprenota_sql.spresult attribute)":[[0,"prenota_sprenota_sql.SPResult.rc",false]],"refresh() (reset_corsie.resetcorsiewindow method)":[[0,"reset_corsie.ResetCorsieWindow.refresh",false]],"refresh_all() (view_celle_multiple.cellemultiplewindow method)":[[0,"view_celle_multiple.CelleMultipleWindow.refresh_all",false]],"reload_from_db() (gestione_pickinglist.gestionepickinglistframe method)":[[0,"gestione_pickinglist.GestionePickingListFrame.reload_from_db",false]],"reset_corsie":[[0,"module-reset_corsie",false]],"resetcorsiewindow (class in reset_corsie)":[[0,"reset_corsie.ResetCorsieWindow",false]],"rows_models (gestione_pickinglist.gestionepickinglistframe attribute)":[[0,"gestione_pickinglist.GestionePickingListFrame.rows_models",false]],"run() (gestione_aree_frame_async.asyncrunner method)":[[0,"gestione_aree_frame_async.AsyncRunner.run",false]],"scrolltable (class in gestione_pickinglist)":[[0,"gestione_pickinglist.ScrollTable",false]],"search_pallets":[[0,"module-search_pallets",false]],"searchwindow (class in search_pallets)":[[0,"search_pallets.SearchWindow",false]],"set_checked() (gestione_pickinglist.plrow method)":[[0,"gestione_pickinglist.PLRow.set_checked",false]],"show() (gestione_aree_frame_async.busyoverlay method)":[[0,"gestione_aree_frame_async.BusyOverlay.show",false]],"sp_xexepackinglistpallet_async() (in module prenota_sprenota_sql)":[[0,"prenota_sprenota_sql.sp_xExePackingListPallet_async",false]],"spresult (class in prenota_sprenota_sql)":[[0,"prenota_sprenota_sql.SPResult",false]],"start() (gestione_pickinglist.toolbarspinner method)":[[0,"gestione_pickinglist.ToolbarSpinner.start",false]],"stop() (gestione_pickinglist.toolbarspinner method)":[[0,"gestione_pickinglist.ToolbarSpinner.stop",false]],"stop_global_loop() (in module gestione_aree_frame_async)":[[0,"gestione_aree_frame_async.stop_global_loop",false]],"title (gestione_pickinglist.colspec attribute)":[[0,"gestione_pickinglist.ColSpec.title",false]],"toolbarspinner (class in gestione_pickinglist)":[[0,"gestione_pickinglist.ToolbarSpinner",false]],"view_celle_multiple":[[0,"module-view_celle_multiple",false]],"widget() (gestione_pickinglist.toolbarspinner method)":[[0,"gestione_pickinglist.ToolbarSpinner.widget",false]],"width (gestione_pickinglist.colspec attribute)":[[0,"gestione_pickinglist.ColSpec.width",false]]},"objects":{"":[[0,0,0,"-","async_msssql_query"],[0,0,0,"-","gestione_aree_frame_async"],[0,0,0,"-","gestione_pickinglist"],[0,0,0,"-","layout_window"],[0,0,0,"-","main"],[0,0,0,"-","prenota_sprenota_sql"],[0,0,0,"-","reset_corsie"],[0,0,0,"-","search_pallets"],[0,0,0,"-","view_celle_multiple"]],"async_msssql_query":[[0,1,1,"","AsyncMSSQLClient"],[0,3,1,"","make_mssql_dsn"]],"async_msssql_query.AsyncMSSQLClient":[[0,2,1,"","dispose"],[0,2,1,"","exec"],[0,2,1,"","query_json"]],"gestione_aree_frame_async":[[0,1,1,"","AsyncRunner"],[0,1,1,"","BusyOverlay"],[0,3,1,"","get_global_loop"],[0,3,1,"","stop_global_loop"]],"gestione_aree_frame_async.AsyncRunner":[[0,2,1,"","close"],[0,2,1,"","run"]],"gestione_aree_frame_async.BusyOverlay":[[0,2,1,"","hide"],[0,2,1,"","show"]],"gestione_pickinglist":[[0,1,1,"","ColSpec"],[0,1,1,"","GestionePickingListFrame"],[0,1,1,"","PLRow"],[0,1,1,"","ScrollTable"],[0,1,1,"","ToolbarSpinner"],[0,3,1,"","create_frame"]],"gestione_pickinglist.ColSpec":[[0,4,1,"","anchor"],[0,4,1,"","key"],[0,4,1,"","title"],[0,4,1,"","width"]],"gestione_pickinglist.GestionePickingListFrame":[[0,2,1,"","on_export"],[0,2,1,"","on_prenota"],[0,2,1,"","on_row_checked"],[0,2,1,"","on_sprenota"],[0,2,1,"","reload_from_db"],[0,4,1,"","rows_models"]],"gestione_pickinglist.PLRow":[[0,2,1,"","build_checkbox"],[0,2,1,"","is_checked"],[0,2,1,"","set_checked"]],"gestione_pickinglist.ScrollTable":[[0,4,1,"","GRID_COLOR"],[0,4,1,"","PADX_L"],[0,4,1,"","PADX_R"],[0,4,1,"","PADY"],[0,2,1,"","add_row"],[0,2,1,"","clear_rows"]],"gestione_pickinglist.ToolbarSpinner":[[0,4,1,"","FRAMES"],[0,2,1,"","start"],[0,2,1,"","stop"],[0,2,1,"","widget"]],"layout_window":[[0,1,1,"","LayoutWindow"],[0,3,1,"","open_layout_window"],[0,3,1,"","pct_text"]],"layout_window.LayoutWindow":[[0,2,1,"","destroy"]],"main":[[0,1,1,"","Launcher"],[0,3,1,"","open_pickinglist_window"]],"prenota_sprenota_sql":[[0,1,1,"","SPResult"],[0,3,1,"","sp_xExePackingListPallet_async"]],"prenota_sprenota_sql.SPResult":[[0,4,1,"","id_result"],[0,4,1,"","message"],[0,4,1,"","rc"]],"reset_corsie":[[0,1,1,"","ResetCorsieWindow"],[0,3,1,"","open_reset_corsie_window"]],"reset_corsie.ResetCorsieWindow":[[0,2,1,"","refresh"]],"search_pallets":[[0,1,1,"","SearchWindow"],[0,3,1,"","open_search_window"]],"view_celle_multiple":[[0,1,1,"","CelleMultipleWindow"],[0,3,1,"","open_celle_multiple_window"]],"view_celle_multiple.CelleMultipleWindow":[[0,2,1,"","collapse_all"],[0,2,1,"","expand_all"],[0,2,1,"","export_to_xlsx"],[0,2,1,"","refresh_all"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","function","Python function"],"4":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:function","4":"py:attribute"},"terms":{"A":0,"I":[1,2,3],"No":0,"The":0,"This":0,"When":0,"__init__":[],"_ask_reset":[],"_bind_ev":[],"_build_layout":[],"_build_matrix_host":[],"_build_stat":[],"_build_top":[],"_build_ui":[],"_do_reset":[],"_do_search":[],"_ensure_engin":[],"_err":[],"_export_xlsx":[],"_fill_cell":[],"_fill_corsi":[],"_fill_pallet":[],"_fill_riepilogo":[],"_first_show":[],"_highlight_cell_by_label":[],"_load_celle_for_corsia":[],"_load_corsi":[],"_load_matrix":[],"_load_pallet_for_cella":[],"_load_riepilogo":[],"_loophold":[],"_ok":[],"_on_heading_double_click":[],"_on_open_nod":[],"_on_select":[],"_rebuild_matrix":[],"_recolor_row_by_documento":[],"_refresh_detail":[],"_refresh_mid_row":[],"_refresh_stat":[],"_req_count":9,"_run":[],"_search_udc":[],"_sort_by_column":[],"abstracteventloop":0,"access":0,"accesso":5,"across":0,"ad":2,"add_row":0,"after_idl":7,"aggiorna":[],"aggiornamento":[],"aggiornano":3,"aggiornino":9,"ai":1,"aioodbc":0,"aisl":0,"al":[1,5],"alla":10,"alloc":0,"alreadi":0,"alto":2,"altr":[],"ampi":12,"anch":1,"anchor":0,"andar":0,"ani":0,"anim":0,"animazion":0,"annulla":[],"api":14,"append":0,"applic":0,"applicativi":8,"applicativo":[2,14],"applicazion":10,"aprono":10,"architettura":14,"architettural":14,"as_dict_row":0,"asincroni":2,"asincrono":[1,3,5,6,10],"assign":0,"async":[0,1,2,6,8,9,14],"async_loop_singleton":[8,14],"async_msssql_queri":[8,14],"asyncio":[0,4],"asyncmssqlcli":[0,5],"asyncreq":[],"asyncrunn":[0,1,2,6,7,11],"attender":0,"attesa":6,"atteso":[],"attiva":9,"attivit\u00e0":0,"attivo":0,"autodoc":0,"automatico":0,"avail":0,"avoid":0,"avvia":[],"avviati":2,"avvio":[],"await":0,"azion":[],"azzera":[],"b":[],"backend":12,"background":0,"bar":0,"barcod":0,"base":0,"baseexcept":0,"becaus":0,"belong":0,"bind":0,"block":0,"bodi":0,"bool":0,"bound":0,"build":0,"build_checkbox":0,"bundl":0,"busi":0,"busyoverlay":[0,6,7],"c":[],"call_soon_threadsaf":[],"callabl":0,"callback":[0,3,5,6,9],"caller":0,"cambio":0,"campi":[],"cancellar":11,"capo":0,"caricamento":7,"caricar":7,"caricata":9,"caricato":13,"cartella":[1,2],"cell":[0,9,13],"cella":[0,13],"cellemultiplewindow":0,"central":0,"centralizza":[1,5],"cercar":[9,12],"chang":0,"che":[1,3,6,9,10,13],"check":[],"checkbox":0,"checkbox_build":0,"chiama":[],"chiamant":[],"chiamata":2,"chiamato":10,"chiarament":11,"chiesta":12,"child":0,"chiusura":10,"class":0,"clear":0,"clear_row":0,"click":[],"client":[0,1,3,10,11],"close":0,"code":0,"codic":[2,14],"codici":12,"col_txt":[],"collaps":0,"collapse_al":0,"collega":[1,6],"collegati":11,"colonna":0,"colorato":0,"colspec":0,"column":0,"come":[9,12],"commit":0,"compat":0,"compila":[],"complessiva":14,"complet":0,"completata":[],"completato":[],"component":6,"comun":3,"con":[0,7,10,12],"concentrano":1,"concern":0,"concettualment":4,"condiviso":[1,3,4,6,10,11],"conferma":[11,12],"configura":[],"confirm":0,"conn_str":0,"connect":0,"connessioni":5,"consent":[9,12],"contain":0,"contatori":[],"contengono":13,"contenuti":13,"contien":[1,2],"coroutin":[0,6],"corrent":9,"corretto":[],"corsi":[0,9],"corsia":[0,9,11,13],"corso":0,"costo":13,"costruisc":10,"costruzion":5,"count":0,"counter":0,"crea":10,"creat":0,"create_async_engin":[],"create_fram":0,"create_pickinglist_fram":[],"creation":0,"creato":[3,5,10],"cross":0,"ctk":0,"ctkcheckbox":0,"ctkframe":0,"ctklabel":0,"ctktoplevel":0,"current":0,"customtkint":0,"d":[],"d0d5dd":0,"da":[1,2,3,4,6,9],"daemon":[],"dai":0,"dal":[1,7,11,14],"dall":6,"databas":[0,1,2,3,5,10],"dati":[],"db":[0,1,2,8,14],"db_app":[0,10],"db_client":0,"dbclient":[],"dedicato":[4,12],"default":[],"defer":0,"dei":[2,7,8],"del":[0,1,4,5,6,10,13,14],"delegano":1,"delet":[0,11],"delicato":11,"dell":[7,9,10],"della":[7,9],"describ":0,"descriv":3,"descrivono":2,"deseleziona":[],"desktop":[0,10],"destroy":[0,9],"destruct":0,"detail":0,"detail_doc":7,"determina":[],"dettagliata":1,"dettaglio":[7,13],"di":[0,1,2,4,11,12,14],"diagram":14,"diagramma":3,"diagrammi":[2,8,14],"dict":0,"dictionari":0,"differ":[0,7],"differito":7,"direttament":[0,2,6],"dismiss":0,"dispatch":0,"display":0,"dispos":[0,10],"distruttiva":11,"diversi":5,"dml":0,"doc":1,"docstr":0,"documentazion":[2,14],"documenti":7,"documento":0,"dopo":11,"doppia":[0,11],"doppio":[],"driver":0,"dsn":[0,5],"due":0,"duplic":0,"duplicata":13,"dure":0,"dynam":0,"e":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"echo":0,"editor":2,"elaps":0,"elenco":7,"empti":0,"encrypt":0,"engin":[0,5],"ensur":[],"enter":0,"entir":0,"entri":0,"error":0,"esegu":11,"eseguito":4,"eseguono":3,"esist":4,"esistent":[],"espansion":[],"esplora":13,"esportar":12,"esser":[2,12],"estrarr":0,"etichetta":0,"event":0,"evento":[],"eventual":[],"evidenziano":2,"evidenziata":12,"evita":[5,6],"evitar":9,"excel":0,"exec":0,"execut":0,"expand":0,"expand_al":0,"explicit":0,"explor":0,"export":[0,9,13],"export_to_xlsx":0,"expos":0,"extra_odbc_kv":0,"f":[],"fa":6,"factori":0,"fals":0,"fase":11,"fila":0,"fila_txt":[],"file":2,"filtri":12,"finch\u00e9":0,"finestr":[3,6,10],"finestra":[9,10,11],"fino":1,"first":0,"flicker":0,"float":0,"flow":[1,14],"flowchart":[],"fluida":7,"flusso":[2,14],"focus":0,"format":0,"formato":5,"fornisc":6,"frame":0,"friend":0,"fuori":9,"futur":[0,6],"futura":2,"g":[],"general":14,"generato":14,"gestion":4,"gestione_aree_frame_async":[4,8,14],"gestione_pickinglist":[1,2,8,14],"gestionepickinglistfram":0,"gestisc":7,"gestiscano":6,"get_global_loop":0,"ghost":[],"gia":5,"git":2,"gi\u00e0":9,"global":[0,4,6,9,10],"globali":[],"graphic":0,"grid_color":0,"gui":[0,1,3,5,6],"h":[],"handl":0,"header":[],"helper":[0,4],"hide":0,"highlight":0,"holder":0,"host":0,"id_result":0,"idcella":12,"idoperator":0,"il":[1,2,3,4,6,7,9,10,11,12,13],"implement":0,"importanti":2,"imposta":[],"inclusi":2,"indic":0,"indicar":0,"informativo":[],"infrastruttura":[2,6,8,14],"infrastruttural":1,"infrastrutturali":8,"ingresso":10,"init":[],"initi":0,"inizial":[7,10,13],"inizializza":10,"inserisc":[],"inspect":0,"instanc":0,"int":0,"intent":0,"interact":0,"interfaccia":2,"is_check":0,"ispezion":11,"istanzia":[],"j":[],"join":[],"json":0,"just":0,"k":[],"keep":0,"kept":0,"key":0,"l":[1,5,6,7,9,13],"la":[0,1,3,5,7,9,10,11,13],"label":0,"lato":11,"launcher":[0,1,3,10],"layer":0,"layout":[0,9,10],"layout_window":[2,8,14],"layoutwindow":0,"lazi":0,"lazili":0,"le":[3,5,10,11,12,13],"legg":13,"leggerissima":0,"letti":2,"lifecycl":0,"lightweight":0,"like":0,"list":[0,7],"listbox":[],"livello":[1,2],"lo":[4,7,9,10],"load":0,"loadcell":[],"loadcorsi":[],"loadmatrix":[],"loadpallet":[],"log":0,"logic":0,"logica":1,"logpackinglist":[],"long":0,"loop":[0,1,3,4,5,6,10],"lot":0,"lotti":12,"lr":[],"lunghi":1,"m":[],"ma":4,"magazzinipallet":[0,11],"magazzino":12,"main":[1,2,4,8,14],"mainten":0,"mainwin":[],"make_mssql_dsn":0,"manag":0,"mantener":7,"mantien":4,"marca":9,"mark":0,"markdown":[2,14],"master":0,"match":0,"matric":[0,9],"matrix_st":[],"matrixst":[],"mediseawal":[],"memoria":9,"mentr":9,"mermaid":[2,8,14],"messag":0,"messaggio":[],"metodo":[],"micro":0,"mid":[],"millisecond":0,"minim":0,"minimal":4,"mirror":0,"misc":0,"mkdoc":2,"modal":0,"model":0,"modo":10,"modul":0,"moduli":[0,1,2,3,6,8,10],"modulo":[1,4,5,6,7,9,11,12,13],"molti":2,"molto":12,"mostra":[0,9,11],"mssql":0,"multipl":0,"n":[],"name":0,"nascosto":10,"need":0,"nei":2,"nel":3,"nella":13,"nessuna":[],"new_event_loop":[],"nient":[],"node":0,"nodi":2,"nodo":[],"non":[2,9,13],"none":0,"normalizza":[],"nullpool":[0,5],"o":2,"object":0,"occup":0,"occupazion":9,"occupi":0,"odbc":0,"ogni":[0,2],"older":0,"on_check":0,"on_error":0,"on_export":0,"on_prenota":0,"on_row_check":0,"on_sprenota":0,"on_success":0,"one":0,"op":0,"open":0,"open_celle_multiple_window":0,"open_layout_window":0,"open_pickinglist_window":[0,10],"open_reset_corsie_window":0,"open_search_window":0,"operativi":10,"operazion":0,"operazioni":11,"option":0,"ordin":9,"organizzando":13,"origin":0,"osservazioni":14,"otherwis":0,"ottien":[],"overlay":[0,6],"p_doubl":0,"p_full":0,"pack":0,"padi":0,"padx_l":0,"padx_r":0,"pagina":1,"pallet":[0,12,13],"param":0,"paramet":0,"parametri":[],"parent":0,"partendo":1,"passa":1,"passaggi":2,"passano":11,"passato":10,"password":0,"payload":0,"pct_text":0,"per":[0,1,5,7,9,10,12],"percentag":0,"perch\u00e9":11,"permett":[7,9,11],"piano":[],"pick":[0,7],"pickfactori":[],"piena":0,"piu":6,"pi\u00f9":[2,9,11,13],"pl":0,"placehold":0,"plrow":0,"poi":10,"point":0,"polici":[],"poll":[],"pont":6,"pool":0,"popola":[],"port":0,"possono":[2,12],"pren":[],"prenota":[],"prenota_sprenota_sql":[1,14],"prenotar":7,"prenotazion":1,"prepara":[],"present":0,"previsto":13,"primo":[0,5],"principal":12,"principali":[0,1,2],"problemi":5,"procedur":0,"prodotto":12,"product":0,"progetto":[0,1,14],"programmat":0,"progress":0,"project":0,"pronto":[5,10],"pulsant":0,"pulsanti":10,"punto":10,"py":[1,2,8,14],"python":[0,14],"quando":3,"quella":[9,11],"queri":[0,1,3,9,11,13],"query_json":[0,2,5],"questa":[1,2,8,14],"questo":[3,4,5,6,7,9,11,12,13],"quindi":[2,6],"raccogli":[8,14],"rappresentano":2,"rc":0,"re":0,"readi":[],"real":5,"rebuild":[],"record":11,"recycl":0,"refer":0,"refresh":[0,7],"refresh_al":0,"refreshstat":[],"releas":0,"reli":0,"reload":0,"reload_from_db":0,"remov":0,"rend":[10,13],"render":0,"renderizzati":2,"reserv":0,"reset":0,"reset_corsi":[2,8,14],"resetcorsiewindow":0,"resiz":0,"restituisc":5,"return":0,"ricalcolata":9,"ricerca":0,"ricerch":12,"richiesta":13,"richiesto":[],"riduc":13,"ridurr":[7,10],"rientrano":3,"riep":[],"riepilogo":[11,13],"riferimenti":[],"riferimento":14,"riga":[0,2,7],"righ":0,"rimuover":[],"riordina":[],"riporta":[],"rispost":9,"risultati":[12,13],"risultato":[],"ritorna":[],"riusa":[],"riusato":6,"riuso":5,"root":0,"row":0,"row_index":0,"rows_model":0,"run":0,"run_coroutine_threadsaf":[],"run_forev":[],"runner":[0,6],"ruolo":4,"s":0,"safe":0,"salta":[],"salva":[],"scalabil":13,"schedul":0,"schemi":2,"scritti":2,"scrive":[],"scrolltabl":0,"se":0,"search":0,"search_pallet":[2,8,14],"searchwindow":0,"section":0,"seguent":0,"select":0,"selezion":[],"seleziona":[],"selezionata":[0,7],"semi":0,"sempr":3,"senza":[0,12],"separa":11,"separata":7,"separato":4,"server":[0,5],"set":[],"set_check":0,"set_event_loop":[],"sezion":[0,8],"sfarfallio":[7,10],"share":0,"short":0,"show":0,"shown":0,"si":1,"sia":13,"singl":0,"singleton":0,"singola":2,"smooth":0,"sola":[0,3,10,13],"solo":[5,10],"sono":2,"sourc":0,"sp":[],"sp_xexepackinglistpallet_async":0,"sphinx":2,"spinner":[0,7],"spren":[],"spresult":0,"sql":[0,5,9],"sql_celle_dup_per_corsia":[],"sql_corsi":[],"sql_count_delet":[],"sql_delet":[],"sql_dettaglio":[],"sql_pallet_in_cella":[],"sql_pl":[],"sql_pl_detail":[],"sql_riepilogo":[],"sql_riepilogo_percentuali":[],"sql_search":[],"sqlalchemi":0,"sqlserver":[],"start":0,"stat":[],"state":0,"statement":0,"statistica":9,"statistich":0,"stato":9,"status":0,"stesso":4,"stile":12,"stop":0,"stop_global_loop":0,"store":0,"str":0,"stringa":[],"su":[0,4,12],"successo":[],"sul":[3,10],"sulla":1,"sum":[],"sum_tbl":[],"summar":0,"summari":0,"support":0,"svolg":4,"svuota":[],"tabella":13,"tabl":0,"tardiv":9,"task":[0,1],"td":[],"tenuta":7,"testo":[],"testual":[],"text":0,"thin":0,"thread":[0,3,4,6],"three":0,"throughout":0,"tie":0,"time":0,"titl":0,"tk":[0,3,6],"togeth":0,"toggl":0,"token":9,"tool":0,"toolbar":[],"toolbarspinn":[0,7],"top":[],"toplevel":0,"tra":[2,3,5,6],"tramit":[5,7,11],"transpar":0,"tree":0,"treeview":12,"trigger":0,"trovata":[],"true":0,"trust_server_certif":0,"tutt":[3,10,11],"tutti":3,"tutto":[12,13],"type":0,"udc":[0,7,12],"udc1":[],"ui":[0,1,3,7,9],"un":[0,4,5,6,9,13],"una":[0,1,2,3,7,9,10,11,13],"unica":1,"unico":1,"uno":12,"unregist":0,"unreserv":0,"updat":0,"usa":[0,7,9,12],"usata":6,"usato":[3,4],"use":0,"user":0,"utent":[],"val":0,"valu":0,"vecchi":9,"veder":7,"verifica":[],"vien":[5,7,9,10,12],"view_celle_multipl":[2,8,14],"viewer":0,"visibil":10,"visibili":[],"visibl":0,"vista":[7,14],"visualizza":9,"visualizzazion":0,"volta":[3,10],"vuota":0,"vuoti":[],"warehous":0,"whether":0,"widget":0,"width":0,"window":0,"wire":0,"work":0,"workbook":[],"xlsx":0,"\u00e8":[0,1,3,7,10,11,13]},"titles":["Riferimento API","Architettura Complessiva","Flow Diagrams","Infrastruttura Async / DB","async_loop_singleton.py","async_msssql_query.py","gestione_aree_frame_async.py","gestione_pickinglist.py","Flow Diagrams","layout_window.py","main.py","reset_corsie.py","search_pallets.py","view_celle_multiple.py","Warehouse Documentation"],"titleterms":{"albero":13,"apertura":7,"api":0,"applicativo":1,"architettura":1,"architettural":1,"async":3,"async_loop_singleton":4,"async_msssql_queri":[0,5],"chiamata":[5,7,9,10,13],"chiusura":4,"complessiva":1,"componenti":6,"contenuti":14,"convenzioni":2,"db":3,"dell":13,"detail":7,"di":[5,6,7,9,10,13],"diagram":[2,8],"distruttivo":11,"document":14,"ed":12,"essenzial":9,"export":12,"flow":[2,8],"flusso":[1,3,4,5,6,7,9,10,11,12,13],"general":1,"gestione_aree_frame_async":[0,6],"gestione_pickinglist":[0,7],"indic":2,"infrastruttura":3,"infrastruttural":6,"layout_window":[0,9],"lazi":13,"load":13,"main":[0,10],"master":7,"note":[3,4,5,6,7,9,10,11,12,13],"operativo":[9,11,12,13],"ordinamento":12,"osservazioni":1,"prenota_sprenota_sql":0,"prenotazion":7,"principal":10,"principali":3,"py":[0,4,5,6,7,9,10,11,12,13],"relazioni":3,"reset_corsi":[0,11],"ricerca":9,"riferimento":0,"s":7,"schema":[5,6,7,9,10,13],"scopo":[3,4,5,6,7,9,10,11,12,13],"search_pallet":[0,12],"trasversal":3,"udc":9,"utilizzo":5,"view_celle_multipl":[0,13],"vista":1,"warehous":14}}) \ No newline at end of file diff --git a/docs/api_reference.rst b/docs/api_reference.rst new file mode 100644 index 0000000..d790b83 --- /dev/null +++ b/docs/api_reference.rst @@ -0,0 +1,77 @@ +Riferimento API +=============== + +La sezione seguente usa ``autodoc`` per estrarre docstring direttamente dai +moduli Python principali del progetto. + +main.py +------- + +.. automodule:: main + :members: + :undoc-members: + :show-inheritance: + +async_msssql_query.py +--------------------- + +.. automodule:: async_msssql_query + :members: + :undoc-members: + :show-inheritance: + +gestione_aree_frame_async.py +---------------------------- + +.. automodule:: gestione_aree_frame_async + :members: + :undoc-members: + :show-inheritance: + +layout_window.py +---------------- + +.. automodule:: layout_window + :members: + :undoc-members: + :show-inheritance: + +reset_corsie.py +--------------- + +.. automodule:: reset_corsie + :members: + :undoc-members: + :show-inheritance: + +view_celle_multiple.py +---------------------- + +.. automodule:: view_celle_multiple + :members: + :undoc-members: + :show-inheritance: + +search_pallets.py +----------------- + +.. automodule:: search_pallets + :members: + :undoc-members: + :show-inheritance: + +gestione_pickinglist.py +----------------------- + +.. automodule:: gestione_pickinglist + :members: + :undoc-members: + :show-inheritance: + +prenota_sprenota_sql.py +----------------------- + +.. automodule:: prenota_sprenota_sql + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..4681e09 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,51 @@ +# Architettura Complessiva + +Questa pagina collega i moduli principali del progetto in una vista unica, +partendo dal launcher fino ai moduli GUI e al livello infrastrutturale async/DB. + +## Vista architetturale + +```{mermaid} +flowchart TD + Main["main.py"] --> Launcher["Launcher"] + Main --> Loop["async_loop_singleton.get_global_loop()"] + Main --> DB["AsyncMSSQLClient"] + + Launcher --> Reset["reset_corsie.py"] + Launcher --> Layout["layout_window.py"] + Launcher --> Ghost["view_celle_multiple.py"] + Launcher --> Search["search_pallets.py"] + Launcher --> Picking["gestione_pickinglist.py"] + + Reset --> Runner["gestione_aree_frame_async.AsyncRunner"] + Layout --> Runner + Ghost --> Runner + Search --> Runner + Picking --> Runner + + Runner --> Loop + Runner --> DB + Picking --> SP["prenota_sprenota_sql.py"] + SP --> DB + DB --> SQL["SQL Server / Mediseawall"] +``` + +## Flusso applicativo generale + +```{mermaid} +flowchart LR + User["Utente"] --> MainWin["Launcher"] + MainWin --> Module["Finestra modulo"] + Module --> AsyncReq["AsyncRunner.run(...)"] + AsyncReq --> DbClient["AsyncMSSQLClient"] + DbClient --> SqlServer["Database SQL Server"] + SqlServer --> Callback["Callback _ok/_err"] + Callback --> Module +``` + +## Osservazioni + +- `main.py` centralizza il loop asincrono e il client database condiviso. +- I moduli GUI si concentrano sulla UI e delegano query e task lunghi a `AsyncRunner`. +- `gestione_pickinglist.py` è l'unico modulo che passa anche da `prenota_sprenota_sql.py` per la logica di prenotazione. +- La cartella `docs/flows/` contiene la vista dettagliata modulo per modulo. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..28d3922 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,40 @@ +"""Sphinx configuration for the warehouse project documentation.""" + +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) + +project = "warehouse" +author = "Project Team" +release = "0.0.1" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "myst_parser", + "sphinxcontrib.mermaid", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +html_theme = "alabaster" +html_static_path = ["_static"] + +autodoc_member_order = "bysource" +autodoc_typehints = "description" +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +myst_enable_extensions = [ + "colon_fence", +] + +mermaid_output_format = "raw" diff --git a/docs/flows/README.md b/docs/flows/README.md new file mode 100644 index 0000000..8b1c017 --- /dev/null +++ b/docs/flows/README.md @@ -0,0 +1,27 @@ +# Flow Diagrams + +Questa cartella contiene schemi di flusso e schemi di chiamata dei moduli +principali avviati da `main.py`. + +I diagrammi sono scritti in Mermaid, quindi possono essere: + +- letti direttamente nei file Markdown; +- renderizzati da molti editor Git/Markdown; +- inclusi in una futura documentazione Sphinx o MkDocs. + +## Indice + +- [main](./main_flow.md) +- [layout_window](./layout_window_flow.md) +- [reset_corsie](./reset_corsie_flow.md) +- [view_celle_multiple](./view_celle_multiple_flow.md) +- [search_pallets](./search_pallets_flow.md) +- [gestione_pickinglist](./gestione_pickinglist_flow.md) +- [infrastruttura async/db](./async_db_flow.md) + +## Convenzioni + +- I diagrammi descrivono il flusso applicativo ad alto livello. +- Non rappresentano ogni singola riga di codice. +- I nodi `AsyncRunner` e `query_json` evidenziano i passaggi asincroni più + importanti tra interfaccia e database. diff --git a/docs/flows/async_db_flow.md b/docs/flows/async_db_flow.md new file mode 100644 index 0000000..f0de6ed --- /dev/null +++ b/docs/flows/async_db_flow.md @@ -0,0 +1,39 @@ +# Infrastruttura Async / DB + +## Scopo + +Questo diagramma descrive il flusso comune usato da tutti i moduli GUI quando +eseguono una query sul database. + +## Flusso trasversale + +```{mermaid} +flowchart TD + A["Evento UI (click / selezione / ricerca)"] --> B["Metodo finestra"] + B --> C["AsyncRunner.run(awaitable)"] + C --> D["Coroutines sul loop globale"] + D --> E["AsyncMSSQLClient.query_json() / exec()"] + E --> F["SQL Server"] + F --> G["Risultato query"] + G --> H["Future completata"] + H --> I["Callback _ok / _err su thread Tk"] + I --> J["Aggiornamento widget"] +``` + +## Relazioni principali + +```{mermaid} +flowchart LR + Main["main.py"] --> Loop["get_global_loop()"] + Main --> DB["AsyncMSSQLClient"] + Windows["Moduli GUI"] --> Runner["AsyncRunner"] + Runner --> Loop + Runner --> DB + DB --> SQL["SQL Server Mediseawall"] +``` + +## Note + +- Il loop asincrono è condiviso tra tutte le finestre. +- Il client DB è condiviso e creato una sola volta nel launcher. +- I callback che aggiornano la UI rientrano sempre sul thread Tk. diff --git a/docs/flows/async_loop_singleton_flow.md b/docs/flows/async_loop_singleton_flow.md new file mode 100644 index 0000000..395dfbb --- /dev/null +++ b/docs/flows/async_loop_singleton_flow.md @@ -0,0 +1,39 @@ +# `async_loop_singleton.py` + +## Scopo + +Questo modulo mantiene un loop asyncio globale e condiviso, eseguito su un +thread dedicato. + +## Flusso + +```{mermaid} +flowchart TD + A["Chiamata a get_global_loop()"] --> B{"Loop gia presente?"} + B -- Si --> C["Ritorna loop esistente"] + B -- No --> D["Crea Event ready"] + D --> E["Avvia thread daemon"] + E --> F["_run()"] + F --> G["new_event_loop()"] + G --> H["set_event_loop(loop)"] + H --> I["ready.set()"] + I --> J["loop.run_forever()"] + J --> K["Ritorna loop al chiamante"] +``` + +## Chiusura + +```{mermaid} +flowchart TD + A["stop_global_loop()"] --> B{"Loop attivo?"} + B -- No --> C["Nessuna azione"] + B -- Si --> D["call_soon_threadsafe(loop.stop)"] + D --> E["join del thread"] + E --> F["Azzera riferimenti globali"] +``` + +## Note + +- E un helper minimale usato da `main.py`. +- Il modulo esiste separato da `gestione_aree_frame_async.py`, ma concettualmente + svolge lo stesso ruolo di gestione del loop condiviso. diff --git a/docs/flows/async_msssql_query_flow.md b/docs/flows/async_msssql_query_flow.md new file mode 100644 index 0000000..b53a283 --- /dev/null +++ b/docs/flows/async_msssql_query_flow.md @@ -0,0 +1,41 @@ +# `async_msssql_query.py` + +## Scopo + +Questo modulo centralizza la costruzione del DSN SQL Server e l'accesso +asincrono al database tramite `AsyncMSSQLClient`. + +## Flusso di utilizzo + +```{mermaid} +flowchart TD + A["main.py o modulo chiamante"] --> B["make_mssql_dsn(...)"] + B --> C["Crea stringa mssql+aioodbc"] + C --> D["AsyncMSSQLClient(dsn)"] + D --> E["query_json(...) o exec(...)"] + E --> F["_ensure_engine()"] + F --> G{"Engine gia creato?"} + G -- No --> H["create_async_engine(..., NullPool, loop corrente)"] + G -- Si --> I["Riusa engine esistente"] + H --> J["execute(text(sql), params)"] + I --> J + J --> K["Normalizza rows/columns"] + K --> L["Ritorna payload JSON-friendly"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + DSN["make_mssql_dsn"] --> Client["AsyncMSSQLClient.__init__"] + Client --> Ensure["_ensure_engine"] + Ensure --> Query["query_json"] + Ensure --> Exec["exec"] + Client --> Dispose["dispose"] +``` + +## Note + +- `NullPool` evita problemi di riuso connessioni tra loop diversi. +- L'engine viene creato solo al primo utilizzo reale. +- `query_json()` restituisce un formato gia pronto per le callback GUI. diff --git a/docs/flows/gestione_aree_frame_async_flow.md b/docs/flows/gestione_aree_frame_async_flow.md new file mode 100644 index 0000000..cb10953 --- /dev/null +++ b/docs/flows/gestione_aree_frame_async_flow.md @@ -0,0 +1,45 @@ +# `gestione_aree_frame_async.py` + +## Scopo + +Questo modulo fornisce l'infrastruttura async usata dalle finestre GUI: + +- loop asincrono globale; +- overlay di attesa; +- runner che collega coroutine e callback Tk. + +## Flusso infrastrutturale + +```{mermaid} +flowchart TD + A["Metodo finestra GUI"] --> B["AsyncRunner.run(awaitable)"] + B --> C{"busy overlay richiesto?"} + C -- Si --> D["BusyOverlay.show()"] + C -- No --> E["Salta overlay"] + D --> F["run_coroutine_threadsafe(awaitable, loop globale)"] + E --> F + F --> G["Polling del Future"] + G --> H{"Future completato?"} + H -- No --> G + H -- Si --> I{"Successo o errore?"} + I -- Successo --> J["widget.after(..., on_success)"] + I -- Errore --> K["widget.after(..., on_error)"] + J --> L["BusyOverlay.hide()"] + K --> L +``` + +## Schema di componenti + +```{mermaid} +flowchart LR + Holder["_LoopHolder"] --> Loop["get_global_loop"] + Loop --> Runner["AsyncRunner"] + Overlay["BusyOverlay"] --> Runner + Runner --> GUI["Moduli GUI"] +``` + +## Note + +- Il modulo fa da ponte tra thread Tk e thread del loop asincrono. +- `BusyOverlay` e riusato da piu finestre, quindi e un componente condiviso. +- `AsyncRunner` evita che i moduli GUI gestiscano direttamente i `Future`. diff --git a/docs/flows/gestione_pickinglist_flow.md b/docs/flows/gestione_pickinglist_flow.md new file mode 100644 index 0000000..e5aca94 --- /dev/null +++ b/docs/flows/gestione_pickinglist_flow.md @@ -0,0 +1,69 @@ +# `gestione_pickinglist.py` + +## Scopo + +Questo modulo gestisce la vista master/detail delle picking list e permette di: + +- caricare l'elenco dei documenti; +- vedere il dettaglio UDC della riga selezionata; +- prenotare e s-prenotare una picking list; +- mantenere una UI fluida con spinner e refresh differiti. + +## Flusso di apertura + +```{mermaid} +flowchart TD + A["open_pickinglist_window() da main.py"] --> B["create_pickinglist_frame()"] + B --> C["GestionePickingListFrame.__init__()"] + C --> D["_build_layout()"] + D --> E["after_idle(_first_show)"] + E --> F["reload_from_db(first=True)"] + F --> G["query_json SQL_PL"] + G --> H["_refresh_mid_rows()"] + H --> I["Render tabella master"] +``` + +## Flusso master/detail + +```{mermaid} +flowchart TD + A["Utente seleziona checkbox riga"] --> B["on_row_checked()"] + B --> C["Deseleziona altre righe"] + C --> D["Salva detail_doc"] + D --> E["query_json SQL_PL_DETAILS"] + E --> F["_refresh_details()"] + F --> G["Render tabella dettaglio"] +``` + +## Prenotazione / s-prenotazione + +```{mermaid} +flowchart TD + A["Click Prenota o S-prenota"] --> B["Verifica riga selezionata"] + B --> C["Determina documento e stato atteso"] + C --> D["Chiama sp_xExePackingListPallet_async()"] + D --> E["Aggiorna Celle e LogPackingList sul DB"] + E --> F["SPResult"] + F --> G{"rc == 0?"} + G -- Si --> H["_recolor_row_by_documento()"] + G -- No --> I["Messaggio di errore"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Init["__init__"] --> Build["_build_layout"] + Init --> First["_first_show"] + First --> Reload["reload_from_db"] + Reload --> Mid["_refresh_mid_rows"] + Check["on_row_checked"] --> Details["_refresh_details"] + Pren["on_prenota"] --> SP["sp_xExePackingListPallet_async"] + Spren["on_sprenota"] --> SP +``` + +## Note + +- Il modulo usa `AsyncRunner`, `BusyOverlay` e `ToolbarSpinner`. +- Il caricamento iniziale è differito con `after_idle` per ridurre lo sfarfallio. +- La riga selezionata viene tenuta separata dal dettaglio tramite `detail_doc`. diff --git a/docs/flows/index.rst b/docs/flows/index.rst new file mode 100644 index 0000000..9c2f9b0 --- /dev/null +++ b/docs/flows/index.rst @@ -0,0 +1,20 @@ +Flow Diagrams +============= + +Questa sezione raccoglie i diagrammi Mermaid dei moduli applicativi e +infrastrutturali. + +.. toctree:: + :maxdepth: 1 + + README.md + main_flow.md + layout_window_flow.md + reset_corsie_flow.md + view_celle_multiple_flow.md + search_pallets_flow.md + gestione_pickinglist_flow.md + async_db_flow.md + async_msssql_query_flow.md + gestione_aree_frame_async_flow.md + async_loop_singleton_flow.md diff --git a/docs/flows/layout_window_flow.md b/docs/flows/layout_window_flow.md new file mode 100644 index 0000000..818d079 --- /dev/null +++ b/docs/flows/layout_window_flow.md @@ -0,0 +1,61 @@ +# `layout_window.py` + +## Scopo + +Questo modulo visualizza il layout delle corsie come matrice di celle, mostra +lo stato di occupazione, consente di cercare una UDC e permette l'export della +matrice. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_layout_window()"] --> B["Crea o riporta in primo piano LayoutWindow"] + B --> C["LayoutWindow.__init__()"] + C --> D["Costruisce toolbar, host matrice, statistiche"] + D --> E["_load_corsie()"] + E --> F["AsyncRunner.run(query_json SQL corsie)"] + F --> G["_on_select() sulla corsia iniziale"] + G --> H["_load_matrix(corsia)"] + H --> I["AsyncRunner.run(query_json SQL matrice)"] + I --> J["_rebuild_matrix()"] + J --> K["_refresh_stats()"] +``` + +## Ricerca UDC + +```{mermaid} +flowchart TD + A["Utente inserisce barcode"] --> B["_search_udc()"] + B --> C["query_json ricerca pallet -> corsia/colonna/fila"] + C --> D{"UDC trovata?"} + D -- No --> E["Messaggio informativo"] + D -- Si --> F["Seleziona corsia in listbox"] + F --> G["_load_matrix(corsia)"] + G --> H["_rebuild_matrix()"] + H --> I["_highlight_cell_by_labels()"] +``` + +## Schema di chiamata essenziale + +```{mermaid} +flowchart LR + Init["__init__"] --> Top["_build_top"] + Init --> Host["_build_matrix_host"] + Init --> Stats["_build_stats"] + Init --> LoadCorsie["_load_corsie"] + LoadCorsie --> Select["_on_select"] + Select --> LoadMatrix["_load_matrix"] + LoadMatrix --> Rebuild["_rebuild_matrix"] + Rebuild --> RefreshStats["_refresh_stats"] + Search["_search_udc"] --> LoadMatrix + Export["_export_xlsx"] --> MatrixState["matrix_state / fila_txt / col_txt / udc1"] +``` + +## Note + +- Il modulo usa un token `_req_counter` per evitare che risposte async vecchie + aggiornino la UI fuori ordine. +- La statistica globale viene ricalcolata da query SQL, mentre quella della + corsia corrente usa la matrice già caricata in memoria. +- `destroy()` marca la finestra come non più attiva per evitare callback tardive. diff --git a/docs/flows/main_flow.md b/docs/flows/main_flow.md new file mode 100644 index 0000000..cca4eb6 --- /dev/null +++ b/docs/flows/main_flow.md @@ -0,0 +1,45 @@ +# `main.py` + +## Scopo + +`main.py` è il punto di ingresso dell'applicazione desktop. Inizializza il loop +asincrono condiviso, crea il client database condiviso e costruisce il launcher +con i pulsanti che aprono i moduli operativi. + +## Flusso principale + +```{mermaid} +flowchart TD + A["Avvio di main.py"] --> B["Configura policy asyncio su Windows"] + B --> C["Ottiene loop globale con get_global_loop()"] + C --> D["Imposta il loop come default"] + D --> E["Costruisce DSN SQL Server"] + E --> F["Crea AsyncMSSQLClient condiviso"] + F --> G["Istanzia Launcher"] + G --> H["Mostra finestra principale"] + H --> I{"Click su un pulsante"} + I --> J["open_reset_corsie_window()"] + I --> K["open_layout_window()"] + I --> L["open_celle_multiple_window()"] + I --> M["open_search_window()"] + I --> N["open_pickinglist_window()"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Launcher["Launcher.__init__"] --> Reset["open_reset_corsie_window"] + Launcher --> Layout["open_layout_window"] + Launcher --> Ghost["open_celle_multiple_window"] + Launcher --> Search["open_search_window"] + Launcher --> Pick["open_pickinglist_window"] + Pick --> PickFactory["create_pickinglist_frame"] +``` + +## Note + +- `db_app` viene creato una sola volta e poi passato a tutte le finestre. +- Alla chiusura del launcher viene chiamato `db_app.dispose()` sul loop globale. +- `open_pickinglist_window()` costruisce la finestra in modo nascosto e la rende + visibile solo a layout pronto, per ridurre lo sfarfallio iniziale. diff --git a/docs/flows/reset_corsie_flow.md b/docs/flows/reset_corsie_flow.md new file mode 100644 index 0000000..6ebc169 --- /dev/null +++ b/docs/flows/reset_corsie_flow.md @@ -0,0 +1,45 @@ +# `reset_corsie.py` + +## Scopo + +Questo modulo mostra il riepilogo di una corsia e permette, dopo doppia +conferma, di cancellare i record di `MagazziniPallet` collegati a quella corsia. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_reset_corsie_window()"] --> B["ResetCorsieWindow.__init__()"] + B --> C["_build_ui()"] + C --> D["_load_corsie()"] + D --> E["query_json SQL_CORSIE"] + E --> F["Seleziona corsia iniziale"] + F --> G["refresh()"] + G --> H["query_json SQL_RIEPILOGO"] + G --> I["query_json SQL_DETTAGLIO"] + H --> J["Aggiorna contatori"] + I --> K["Aggiorna tree celle occupate"] +``` + +## Flusso distruttivo + +```{mermaid} +flowchart TD + A["Click su Svuota corsia"] --> B["_ask_reset()"] + B --> C["query_json SQL_COUNT_DELETE"] + C --> D{"Record da cancellare > 0?"} + D -- No --> E["Messaggio: niente da rimuovere"] + D -- Si --> F["Richiesta conferma testuale"] + F --> G{"Testo corretto?"} + G -- No --> H["Annulla operazione"] + G -- Si --> I["_do_reset(corsia)"] + I --> J["query_json SQL_DELETE"] + J --> K["Messaggio completato"] + K --> L["refresh()"] +``` + +## Note + +- È il modulo più delicato lato operazioni, perché esegue `DELETE`. +- La finestra separa chiaramente fase di ispezione e fase distruttiva. +- Tutte le query passano dal client condiviso tramite `AsyncRunner`. diff --git a/docs/flows/search_pallets_flow.md b/docs/flows/search_pallets_flow.md new file mode 100644 index 0000000..dffecb1 --- /dev/null +++ b/docs/flows/search_pallets_flow.md @@ -0,0 +1,43 @@ +# `search_pallets.py` + +## Scopo + +Questo modulo consente di cercare pallet/UDC, lotti e codici prodotto su tutto +il magazzino e di esportare i risultati. + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_search_window()"] --> B["SearchWindow.__init__()"] + B --> C["_build_ui()"] + C --> D["Utente compila filtri"] + D --> E["_do_search()"] + E --> F{"Filtri vuoti?"} + F -- Si --> G["Richiesta conferma ricerca globale"] + F -- No --> H["Prepara parametri SQL"] + G --> H + H --> I["AsyncRunner.run(query_json SQL_SEARCH)"] + I --> J["_ok()"] + J --> K["Popola Treeview"] + K --> L["Eventuale reset campi"] +``` + +## Ordinamento ed export + +```{mermaid} +flowchart TD + A["Doppio click su header"] --> B["_on_heading_double_click()"] + B --> C["_sort_by_column()"] + C --> D["Riordina righe del Treeview"] + + E["Click Export XLSX"] --> F["_export_xlsx()"] + F --> G["Legge righe visibili"] + G --> H["Scrive workbook Excel"] +``` + +## Note + +- Il modulo usa `Treeview` come backend principale. +- Le ricerche possono essere molto ampie: per questo, senza filtri, viene chiesta conferma. +- `IDCella = 9999` viene evidenziata con uno stile dedicato. diff --git a/docs/flows/view_celle_multiple_flow.md b/docs/flows/view_celle_multiple_flow.md new file mode 100644 index 0000000..5dd4187 --- /dev/null +++ b/docs/flows/view_celle_multiple_flow.md @@ -0,0 +1,58 @@ +# `view_celle_multiple.py` + +## Scopo + +Questo modulo esplora le celle che contengono più pallet del previsto, +organizzando i risultati in un albero: + +- corsia +- cella duplicata +- pallet contenuti nella cella + +## Flusso operativo + +```{mermaid} +flowchart TD + A["open_celle_multiple_window()"] --> B["CelleMultipleWindow.__init__()"] + B --> C["_build_layout()"] + C --> D["_bind_events()"] + D --> E["refresh_all()"] + E --> F["_load_corsie()"] + E --> G["_load_riepilogo()"] + F --> H["query_json SQL_CORSIE"] + G --> I["query_json SQL_RIEPILOGO_PERCENTUALI"] + H --> J["_fill_corsie()"] + I --> K["_fill_riepilogo()"] +``` + +## Lazy loading dell'albero + +```{mermaid} +flowchart TD + A["Espansione nodo tree"] --> B["_on_open_node()"] + B --> C{"Nodo corsia o nodo cella?"} + C -- Corsia --> D["_load_celle_for_corsia()"] + D --> E["query_json SQL_CELLE_DUP_PER_CORSIA"] + E --> F["_fill_celle()"] + C -- Cella --> G["_load_pallet_for_cella()"] + G --> H["query_json SQL_PALLET_IN_CELLA"] + H --> I["_fill_pallet()"] +``` + +## Schema di chiamata + +```{mermaid} +flowchart LR + Refresh["refresh_all"] --> Corsie["_load_corsie"] + Refresh --> Riep["_load_riepilogo"] + Open["_on_open_node"] --> LoadCelle["_load_celle_for_corsia"] + Open --> LoadPallet["_load_pallet_for_cella"] + Export["export_to_xlsx"] --> Tree["tree dati dettaglio"] + Export --> Sum["sum_tbl riepilogo"] +``` + +## Note + +- L'albero è caricato a richiesta, non tutto in una sola query. +- Questo riduce il costo iniziale e rende il modulo più scalabile. +- L'export legge sia il dettaglio dell'albero sia la tabella di riepilogo. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7b1fe98 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +Warehouse Documentation +======================= + +Questa documentazione raccoglie: + +- riferimento API generato dal codice Python; +- diagrammi di flusso in Markdown/Mermaid; +- vista architetturale complessiva del progetto. + +.. toctree:: + :maxdepth: 2 + :caption: Contenuti + + architecture + api_reference + flows/index diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..5767bef --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,18 @@ +@ECHO OFF + +set SPHINXBUILD=sphinx-build +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR%/%1 +GOTO end + +:help +ECHO.Usage: make.bat ^ +ECHO. +ECHO.Example: +ECHO. make.bat html + +:end diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..69edf94 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=8.0 +myst-parser>=4.0 +sphinxcontrib-mermaid>=1.0 diff --git a/fix_layout_window.py b/fix_layout_window.py index b0589b9..acc9619 100644 --- a/fix_layout_window.py +++ b/fix_layout_window.py @@ -1,3 +1,10 @@ +"""One-off maintenance script to sanitize ``border_color`` usage in ``layout_window``. + +The script removes incompatible ``border_color='transparent'`` assignments from +widget configuration calls while preserving explicit highlight colors that are +still meaningful for the UI. +""" + import re from pathlib import Path @@ -11,16 +18,16 @@ 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", + re.compile(r""",\s*border_color\s*=\s*["']transparent["']"""), + re.compile(r"""border_color\s*=\s*["']transparent["']\s*,\s*""") ] for pat in patterns: src = pat.sub("", src) -# 2) Se sono rimaste virgole prima della parentesi di chiusura: ", )" -> ")" +# 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 +# 3) Rimuovi anche eventuali border_color=None lasciati da vecchie varianti. patterns_none = [ re.compile(r""",\s*border_color\s*=\s*None"""), re.compile(r"""border_color\s*=\s*None\s*,\s*""") @@ -29,11 +36,12 @@ 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 +# 4) Manteniamo eventuali border_color="blue" usati per l'highlight. # Scrivi backup e nuovo file bak = p.with_suffix(".py.bak_fix_bc_transparent") if not bak.exists(): + # Keep a backup copy before overwriting the target file. bak.write_text(Path(p).read_text(encoding="utf-8"), encoding="utf-8") p.write_text(src, encoding="utf-8") diff --git a/fix_query.py b/fix_query.py index 165272e..377b2fc 100644 --- a/fix_query.py +++ b/fix_query.py @@ -1,3 +1,10 @@ +"""One-off maintenance script to patch performance issues in ``layout_window``. + +The script was used during development to remove an expensive resize-triggered +refresh and to inject some lifecycle guards into the window implementation. +It is kept in the repository as an auditable patch recipe. +""" + from pathlib import Path import re @@ -6,6 +13,7 @@ src = p.read_text(encoding="utf-8") backup = p.with_suffix(".py.bak_perf") if not backup.exists(): + # Preserve the original version so the patch can be reversed manually. backup.write_text(src, encoding="utf-8") # 1) Rimuovi il bind su che innescava refresh continui. @@ -60,8 +68,7 @@ if "def destroy(self):" not in src: ) 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. +# 4) Nei callback _ok/_err delle query, assicurati che non facciano nulla se la finestra è chiusa. src = re.sub( r"def _ok\(res\):\n", "def _ok(res):\n" @@ -77,7 +84,7 @@ src = re.sub( src ) -# 5) Piccola robustezza: prima di schedulare highlight post-ricarica controlla ancora _alive +# 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" diff --git a/gestione_aree_frame_async.py b/gestione_aree_frame_async.py index cd65196..a865525 100644 --- a/gestione_aree_frame_async.py +++ b/gestione_aree_frame_async.py @@ -1,39 +1,52 @@ -# gestione_aree_frame_async.py +"""Shared Tk/async helpers used by multiple warehouse windows. + +The module bundles three concerns used throughout the GUI: + +* lifecycle of the shared background asyncio loop; +* a modal-like busy overlay shown during long-running tasks; +* an ``AsyncRunner`` that schedules coroutines and re-enters Tk safely. +""" + from __future__ import annotations import asyncio import threading import tkinter as tk -import customtkinter as ctk from typing import Any, Callable, Optional +import customtkinter as ctk + __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: + """Keep references to the shared event loop and its worker thread.""" + def __init__(self): self.loop: Optional[asyncio.AbstractEventLoop] = None self.thread: Optional[threading.Thread] = None self.ready = threading.Event() + _GLOBAL = _LoopHolder() + def _run_loop(): + """Create and run the shared event loop inside the worker thread.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) _GLOBAL.loop = loop _GLOBAL.ready.set() loop.run_forever() + def get_global_loop() -> asyncio.AbstractEventLoop: + """Return the shared background event loop, creating it if needed.""" if _GLOBAL.loop is not None: return _GLOBAL.loop _GLOBAL.thread = threading.Thread(target=_run_loop, name="warehouse-asyncio", daemon=True) @@ -43,7 +56,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop: raise RuntimeError("Impossibile avviare l'event loop globale") return _GLOBAL.loop + def stop_global_loop(): + """Stop the shared event loop and release thread references.""" if _GLOBAL.loop and _GLOBAL.loop.is_running(): _GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop) if _GLOBAL.thread: @@ -52,11 +67,12 @@ def stop_global_loop(): _GLOBAL.thread = None _GLOBAL.ready.clear() -# ======================== -# Busy overlay -# ======================== + class BusyOverlay: + """Semi-transparent overlay used to block interaction during async tasks.""" + def __init__(self, parent: tk.Misc): + """Bind the overlay lifecycle to the given parent widget.""" self.parent = parent self._top: Optional[ctk.CTkToplevel] = None self._pb: Optional[ctk.CTkProgressBar] = None @@ -64,6 +80,7 @@ class BusyOverlay: self._bind_id = None def _reposition(self): + """Resize the overlay so it keeps covering the parent toplevel.""" if not self._top: return root = self.parent.winfo_toplevel() @@ -72,7 +89,8 @@ class BusyOverlay: w, h = root.winfo_width(), root.winfo_height() self._top.geometry(f"{w}x{h}+{x}+{y}") - def show(self, message="Attendere…"): + def show(self, message="Attendere..."): + """Display the overlay or just update its message if already visible.""" if self._top: if self._lbl: self._lbl.configure(text=message) @@ -106,6 +124,7 @@ class BusyOverlay: self._bind_id = root.bind("", lambda e: self._reposition(), add="+") def hide(self): + """Dismiss the overlay and unregister resize bindings.""" if self._pb: try: self._pb.stop() @@ -126,12 +145,12 @@ class BusyOverlay: pass self._bind_id = None -# ======================== -# AsyncRunner (single-loop) -# ======================== + class AsyncRunner: - """Run awaitables on the single global loop and callback on Tk main thread.""" + """Run awaitables on the shared loop and callback on Tk's main thread.""" + def __init__(self, widget: tk.Misc): + """Capture the widget used to marshal callbacks back to Tk.""" self.widget = widget self.loop = get_global_loop() @@ -141,8 +160,9 @@ class AsyncRunner: on_success: Callable[[Any], None], on_error: Optional[Callable[[BaseException], None]] = None, busy: Optional[BusyOverlay] = None, - message: str = "Operazione in corso…", + message: str = "Operazione in corso...", ): + """Schedule ``awaitable`` and dispatch completion callbacks in Tk.""" if busy: busy.show(message) fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop) @@ -166,5 +186,5 @@ class AsyncRunner: _poll() def close(self): - # no-op: loop is global + """No-op kept for API compatibility with older callers.""" pass diff --git a/gestione_pickinglist.py b/gestione_pickinglist.py index 48bb032..9549fd0 100644 --- a/gestione_pickinglist.py +++ b/gestione_pickinglist.py @@ -1,4 +1,9 @@ -# =================== gestione_pickinglist.py (NO-FLICKER + UX TUNING + MICRO-SPINNER) =================== +"""Picking list management window. + +The module presents a master/detail UI for packing lists, supports reservation +and unreservation through an async stored-procedure port and keeps rendering +smooth by relying on deferred updates and lightweight progress indicators. +""" from __future__ import annotations import tkinter as tk @@ -10,9 +15,6 @@ 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: @@ -99,10 +101,11 @@ def _rows_to_dicts(res: Dict[str, Any]) -> List[Dict[str, Any]]: return [] def _s(v) -> str: - """Stringify safe: None -> '', altrimenti str(v).""" + """Return a string representation, converting ``None`` to an empty string.""" return "" if v is None else str(v) def _first(d: Dict[str, Any], keys: List[str], default: str = ""): + """Return the first non-empty value found among the provided keys.""" for k in keys: if k in d and d[k] not in (None, ""): return d[k] @@ -111,6 +114,8 @@ def _first(d: Dict[str, Any], keys: List[str], default: str = ""): # -------------------- column specs -------------------- @dataclass class ColSpec: + """Describe one logical column rendered in a ``ScrollTable``.""" + title: str key: str width: int @@ -149,6 +154,7 @@ class ToolbarSpinner: """ FRAMES = ("◐", "◓", "◑", "◒") def __init__(self, parent: tk.Widget): + """Create the spinner label attached to the given parent widget.""" self.parent = parent self.lbl = ctk.CTkLabel(parent, text="", width=28) self._i = 0 @@ -156,9 +162,11 @@ class ToolbarSpinner: self._job = None def widget(self) -> ctk.CTkLabel: + """Return the label widget hosting the spinner animation.""" return self.lbl def start(self, text: str = ""): + """Start the animation and optionally show a short status message.""" if self._active: return self._active = True @@ -166,6 +174,7 @@ class ToolbarSpinner: self._tick() def stop(self): + """Stop the animation and clear the label text.""" self._active = False if self._job is not None: try: @@ -176,6 +185,7 @@ class ToolbarSpinner: self.lbl.configure(text="") def _tick(self): + """Advance the spinner animation frame.""" if not self._active: return self._i = (self._i + 1) % len(self.FRAMES) @@ -196,6 +206,7 @@ class ScrollTable(ctk.CTkFrame): PADY = 2 def __init__(self, master, columns: List[ColSpec]): + """Create a fixed-header scrollable table rendered with Tk/CTk widgets.""" super().__init__(master) self.columns = columns self.total_w = sum(c.width for c in self.columns) @@ -236,6 +247,7 @@ class ScrollTable(ctk.CTkFrame): self._build_header() def _build_header(self): + """Build the static header row using the configured columns.""" for w in self.h_inner.winfo_children(): w.destroy() @@ -260,6 +272,7 @@ class ScrollTable(ctk.CTkFrame): self.h_canvas.configure(scrollregion=(0,0,self.total_w,ROW_H)) def _update_body_width(self): + """Keep the scroll region aligned with the current body content width.""" self.b_canvas.itemconfigure(self.body_window, width=self.total_w) sr = self.b_canvas.bbox("all") if sr: @@ -268,22 +281,27 @@ class ScrollTable(ctk.CTkFrame): self.b_canvas.configure(scrollregion=(0,0,self.total_w,0)) def _on_body_configure(self): + """React to body resize events by syncing dimensions and header scroll.""" self._update_body_width() self._sync_header_width() def _sync_header_width(self): + """Mirror the body horizontal scroll position on the header canvas.""" first, _ = self.b_canvas.xview() self.h_canvas.xview_moveto(first) def _xscroll_both(self, *args): + """Scroll header and body together when the horizontal bar moves.""" self.h_canvas.xview(*args) self.b_canvas.xview(*args) def _xscroll_set_both(self, first, last): + """Update the header viewport and scrollbar thumb in one place.""" self.h_canvas.xview_moveto(first) self.xbar.set(first, last) def clear_rows(self): + """Remove all rendered body rows.""" for w in self.b_inner.winfo_children(): w.destroy() self._update_body_width() @@ -295,6 +313,7 @@ class ScrollTable(ctk.CTkFrame): anchors: Optional[List[str]] = None, checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None, ): + """Append one row to the table body.""" row = ctk.CTkFrame(self.b_inner, fg_color="transparent", height=ROW_H, width=self.total_w) row.pack(fill="x", expand=False) @@ -326,13 +345,24 @@ class ScrollTable(ctk.CTkFrame): # -------------------- PL row model -------------------- class PLRow: + """State holder for one picking list row and its selection checkbox.""" + def __init__(self, pl: Dict[str, Any], on_check): + """Bind a picking list payload to a ``BooleanVar`` and callback.""" 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 is_checked(self) -> bool: + """Return whether the row is currently selected.""" + return self.var.get() + + def set_checked(self, val: bool): + """Programmatically update the checkbox state.""" + self.var.set(val) + def build_checkbox(self, parent) -> ctk.CTkCheckBox: + """Create the checkbox widget bound to this row model.""" return ctk.CTkCheckBox(parent, text="", variable=self.var, command=lambda: self._callback(self, self.var.get())) @@ -340,8 +370,11 @@ class PLRow: # -------------------- main frame (no-flicker + UX tuning + spinner) -------------------- class GestionePickingListFrame(ctk.CTkFrame): def __init__(self, master, *, db_client=None, conn_str=None): + """Create the master/detail picking list frame.""" super().__init__(master) - self.db_client = db_client or _get_db_singleton(get_global_loop(), conn_str) + if db_client is None: + raise ValueError("GestionePickingListFrame richiede un db_client condiviso.") + self.db_client = db_client self.runner = AsyncRunner(self) # runner condiviso (usa loop globale) self.busy = BusyOverlay(self) # overlay collaudato @@ -350,6 +383,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self.detail_doc = None self._first_loading: bool = False # flag per cursore d'attesa solo al primo load + self._render_job = None # Tracking del job di rendering in corso self._build_layout() # 🔇 Niente reload immediato: carichiamo quando la finestra è idle (= già resa) @@ -368,6 +402,7 @@ class GestionePickingListFrame(ctk.CTkFrame): # ---------- UI ---------- def _build_layout(self): + """Build toolbar, master table and detail table.""" for r in (1, 3): self.grid_rowconfigure(r, weight=1) self.grid_columnconfigure(0, weight=1) @@ -394,6 +429,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self._draw_details_hint() def _draw_details_hint(self): + """Render the placeholder row shown when no document is selected.""" self.det_table.clear_rows() self.det_table.add_row( values=["", "", "", "Seleziona una Picking List per vedere le UDC…", "", ""], @@ -414,6 +450,7 @@ class GestionePickingListFrame(ctk.CTkFrame): pass def _refresh_mid_rows(self, rows: List[Dict[str, Any]]): + """Rebuild the master table using the latest query results.""" self.pl_table.clear_rows() self.rows_models.clear() @@ -443,6 +480,7 @@ class GestionePickingListFrame(ctk.CTkFrame): # ----- helpers ----- def _get_selected_model(self) -> Optional[PLRow]: + """Return the currently checked picking list row, if any.""" for m in self.rows_models: if m.is_checked(): return m @@ -480,6 +518,7 @@ class GestionePickingListFrame(ctk.CTkFrame): # ----- eventi ----- def on_row_checked(self, model: PLRow, is_checked: bool): + """Handle row selection changes and refresh the detail section.""" # selezione esclusiva if is_checked: for m in self.rows_models: @@ -493,22 +532,25 @@ class GestionePickingListFrame(ctk.CTkFrame): return await self.db_client.query_json(SQL_PL_DETAILS, {"Documento": self.detail_doc}) def _ok(res): - self.spinner.stop() # spinner OFF + # NON fermare lo spinner subito: lo farà _refresh_details_incremental self._detail_cache[self.detail_doc] = _rows_to_dicts(res) - # differisci il render dei dettagli (più fluido) - self.after_idle(self._refresh_details) + # Avvia il rendering incrementale che mantiene l'overlay attivo + self._refresh_details_incremental() def _err(ex): self.spinner.stop() + self.busy.hide() # Chiudi l'overlay in caso di errore messagebox.showerror("DB", f"Errore nel caricamento dettagli:\n{ex}") self.runner.run( _job(), on_success=_ok, on_error=_err, - busy=self.busy, + busy=None, # NON usare busy automatico: lo gestiamo manualmente nel rendering message=f"Carico UDC per Documento {self.detail_doc}…" ) + # Mostra manualmente l'overlay per la query + self.busy.show(f"Carico UDC per Documento {self.detail_doc}…") else: if not any(m.is_checked() for m in self.rows_models): @@ -517,6 +559,7 @@ class GestionePickingListFrame(ctk.CTkFrame): # ----- load PL ----- def reload_from_db(self, first: bool = False): + """Load or reload the picking list summary table from the database.""" self.spinner.start(" Carico…") # spinner ON async def _job(): return await self.db_client.query_json(SQL_PL, {}) @@ -550,6 +593,7 @@ class GestionePickingListFrame(ctk.CTkFrame): ) def _refresh_details(self): + """Render the detail table for the currently selected document.""" self.det_table.clear_rows() if not self.detail_doc: self._draw_details_hint() @@ -576,8 +620,74 @@ class GestionePickingListFrame(ctk.CTkFrame): anchors=[c.anchor for c in DET_COLS] ) + def _refresh_details_incremental(self, batch_size: int = 25): + """ + Render detail table incrementally in batches to keep UI responsive. + Mantiene l'overlay visibile fino al completamento del rendering. + """ + self.det_table.clear_rows() + if not self.detail_doc: + self._draw_details_hint() + self.spinner.stop() + self.busy.hide() + 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) + self.spinner.stop() + self.busy.hide() + return + + # Inizia il rendering incrementale + total_rows = len(rows) + self.busy.show(f"Rendering {len(rows)} UDC...") + self._render_batch(rows, batch_size, 0, total_rows) + + def _render_batch(self, rows: List[Dict[str, Any]], batch_size: int, start_idx: int, total_rows: int): + """ + Render a batch of rows and schedule the next batch. + Mantiene lo spinner attivo fino all'ultimo batch. + """ + end_idx = min(start_idx + batch_size, total_rows) + + # Aggiorna lo spinner con il progresso + progress_pct = int((end_idx / total_rows) * 100) + self.spinner.lbl.configure(text=f"◐ Rendering {progress_pct}%") + + # Aggiorna anche il messaggio dell'overlay + self.busy.show(f"Rendering {progress_pct}% ({end_idx}/{total_rows} UDC)...") + + # Renderizza il batch corrente + for r in range(start_idx, end_idx): + d = rows[r] + pallet = _s(_first(d, ["Pallet", "UDC", "PalletID"])) + lotto = _s(_first(d, ["Lotto"])) + articolo = _s(_first(d, ["Articolo", "CodArticolo", "CodiceArticolo", "Art", "Codice"])) + descr = _s(_first(d, ["Descrizione", "Descr", "DescrArticolo", "DescArticolo", "DesArticolo"])) + qta = _s(_first(d, ["Qta", "Quantita", "Qty", "QTY"])) + ubi_raw = _first(d, ["Ubicazione", "Cella", "PalletCella"]) + loc = "Non scaffalata" if (ubi_raw is None or str(ubi_raw).strip()=="") else str(ubi_raw).strip() + + self.det_table.add_row( + values=[pallet, lotto, articolo, descr, qta, loc], + row_index=r, + anchors=[c.anchor for c in DET_COLS] + ) + + # Se ci sono ancora righe da renderizzare, schedula il prossimo batch + if end_idx < total_rows: + # Lascia respirare Tk tra i batch (10ms) + self.after(10, lambda: self._render_batch(rows, batch_size, end_idx, total_rows)) + else: + # Ultimo batch completato: ferma lo spinner e chiudi l'overlay + self.spinner.stop() + self.busy.hide() + # ----- azioni ----- def on_prenota(self): + """Reserve the selected picking list.""" model = self._get_selected_model() if not model: messagebox.showinfo("Prenota", "Seleziona una Picking List (checkbox) prima di prenotare.") @@ -617,6 +727,7 @@ class GestionePickingListFrame(ctk.CTkFrame): ) def on_sprenota(self): + """Unreserve the selected picking list.""" model = self._get_selected_model() if not model: messagebox.showinfo("S-prenota", "Seleziona una Picking List (checkbox) prima di s-prenotare.") @@ -656,13 +767,15 @@ class GestionePickingListFrame(ctk.CTkFrame): ) def on_export(self): + """Placeholder for a future export implementation.""" messagebox.showinfo("Esporta", "Stub esportazione.") # factory per main def create_frame(parent, *, db_client=None, conn_str=None) -> 'GestionePickingListFrame': + """Factory used by the launcher to build the picking list frame.""" ctk.set_appearance_mode("light") ctk.set_default_color_theme("green") - return GestionePickingListFrame(parent, db_client=db_client, conn_str=conn_str) + return GestionePickingListFrame(parent, db_client=db_client) # =================== /gestione_pickinglist.py =================== diff --git a/layout_window.py b/layout_window.py index e460a93..bf7425d 100644 --- a/layout_window.py +++ b/layout_window.py @@ -1,3 +1,5 @@ +"""Graphical aisle layout viewer for warehouse cells and pallet occupancy.""" + from __future__ import annotations import tkinter as tk from tkinter import Menu, messagebox, filedialog @@ -15,6 +17,7 @@ FG_LIGHT = "#FFFFFF" def pct_text(p_full: float, p_double: float | None = None) -> str: + """Format occupancy percentages for the progress-bar labels.""" p_full = max(0.0, min(1.0, p_full)) pf = round(p_full * 100, 1) pe = round(100 - pf, 1) @@ -36,6 +39,7 @@ class LayoutWindow(ctk.CTkToplevel): - Export XLSX """ def __init__(self, parent: tk.Widget, db_app): + """Create the window and initialize the state used by the matrix view.""" super().__init__(parent) self.title("Warehouse · Layout corsie") self.geometry("1200x740") @@ -82,6 +86,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- TOP BAR ---------------- def _build_top(self): + """Create the top toolbar with aisle selection and search controls.""" top = ctk.CTkFrame(self) top.grid(row=0, column=0, sticky="nsew", padx=8, pady=6) for i in range(4): @@ -114,6 +119,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- MATRIX HOST ---------------- def _build_matrix_host(self): + """Create the container that will host the dynamically rebuilt matrix.""" center = ctk.CTkFrame(self) center.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 6)) center.grid_rowconfigure(0, weight=1) @@ -122,6 +128,7 @@ class LayoutWindow(ctk.CTkToplevel): self.host.grid(row=0, column=0, sticky="nsew", padx=4, pady=4) def _apply_cell_style(self, btn: ctk.CTkButton, state: int): + """Apply the visual state associated with a cell occupancy level.""" if state == 0: btn.configure( fg_color=COLOR_EMPTY, hover_color="#9A9A9A", @@ -139,6 +146,7 @@ class LayoutWindow(ctk.CTkToplevel): ) def _clear_highlight(self): + """Remove the temporary highlight from the previously focused cell.""" if self._highlighted and self.buttons: r, c = self._highlighted try: @@ -162,6 +170,7 @@ class LayoutWindow(ctk.CTkToplevel): self._highlighted = None def _rebuild_matrix(self, rows: int, cols: int, state, fila_txt, col_txt, desc, udc1, corsia): + """Recreate the visible cell matrix from the latest query result.""" # prima rimuovi highlight su vecchi bottoni self._clear_highlight() # ripulisci host @@ -213,6 +222,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- CONTEXT MENU ---------------- def _open_menu(self, event, r, c): + """Open the context menu for a single matrix cell.""" st = self.matrix_state[r][c] corsia = self.corsia_selezionata.get() label = f"{corsia}.{self.col_txt[r][c]}.{self.fila_txt[r][c]}" @@ -234,6 +244,7 @@ class LayoutWindow(ctk.CTkToplevel): m.tk_popup(x, y) def _set_cell(self, r, c, val): + """Update a cell state in memory and refresh the local statistics.""" self.matrix_state[r][c] = val btn = self.buttons[r][c] self._apply_cell_style(btn, val) @@ -241,6 +252,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- STATS ---------------- def _build_stats(self): + """Create progress bars, labels and legend for occupancy statistics.""" bottom = ctk.CTkFrame(self) bottom.grid(row=2, column=0, sticky="nsew", padx=8, pady=6) bottom.grid_columnconfigure(0, weight=1) @@ -265,6 +277,7 @@ class LayoutWindow(ctk.CTkToplevel): self._legend(leg, 5, "Doppia UDC", COLOR_DOUBLE) def _legend(self, parent, col, text, color): + """Add a legend entry describing one matrix color.""" box = tk.Canvas(parent, width=18, height=12, highlightthickness=0) box.create_rectangle(0, 0, 18, 12, fill=color, width=1, outline="#444") box.grid(row=0, column=col) @@ -272,6 +285,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- DATA LOADING ---------------- def _load_corsie(self): + """Load the list of aisles available for visualization.""" sql = """ WITH C AS ( SELECT DISTINCT LTRIM(RTRIM(Corsia)) AS Corsia @@ -316,6 +330,7 @@ class LayoutWindow(ctk.CTkToplevel): self._async.run(self.db.query_json(sql, {}), _ok, _err, busy=self._busy, message="Carico corsie…") def _on_select(self, _): + """Load the selected aisle when the listbox selection changes.""" sel = self.lb.curselection() if not sel: return @@ -324,6 +339,7 @@ class LayoutWindow(ctk.CTkToplevel): self._load_matrix(corsia) def _select_corsia_in_listbox(self, corsia: str): + """Select a given aisle inside the listbox if it is present.""" for i in range(self.lb.size()): if self.lb.get(i) == corsia: self.lb.selection_clear(0, tk.END) @@ -332,6 +348,7 @@ class LayoutWindow(ctk.CTkToplevel): break def _load_matrix(self, corsia: str): + """Query and render the matrix for the selected aisle.""" # nuovo token richiesta → evita che risposte vecchie spazzino la UI self._req_counter += 1 req_id = self._req_counter @@ -440,6 +457,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- SEARCH ---------------- def _search_udc(self): + """Find a pallet barcode and navigate to the aisle and cell that contain it.""" barcode = (self.search_var.get() or "").strip() if not barcode: self._toast("Inserisci un barcode UDC da cercare.") @@ -488,6 +506,7 @@ class LayoutWindow(ctk.CTkToplevel): self._async.run(self.db.query_json(sql, {"barcode": barcode}), _ok, _err, busy=self._busy, message="Cerco UDC…") def _try_highlight(self, col_txt: str, fila_txt: str) -> bool: + """Highlight a cell by its textual row and column labels.""" for r in range(len(self.col_txt)): for c in range(len(self.col_txt[r])): if self.col_txt[r][c] == col_txt and self.fila_txt[r][c] == fila_txt: @@ -504,15 +523,18 @@ class LayoutWindow(ctk.CTkToplevel): return False def _highlight_cell_by_labels(self, col_txt: str, fila_txt: str): + """Show a toast when a searched cell cannot be highlighted.""" if not self._try_highlight(col_txt, fila_txt): self._toast("Cella trovata ma non mappabile a pulsante.") # ---------------- COMMANDS ---------------- def _refresh_current(self): + """Reload the matrix of the currently selected aisle.""" if self.corsia_selezionata.get(): self._load_matrix(self.corsia_selezionata.get()) def _export_xlsx(self): + """Export both matrix metadata and the rendered grid to Excel.""" if not self.matrix_state: messagebox.showinfo("Export", "Nessuna matrice da esportare.") return @@ -569,6 +591,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- STATS ---------------- def _refresh_stats(self): + """Refresh global and local occupancy statistics shown in the footer.""" # globale dal DB sql_tot = """ WITH C AS ( @@ -612,6 +635,7 @@ class LayoutWindow(ctk.CTkToplevel): self.sel_text.configure(text=pct_text(p_full, p_dbl)) def _draw_bar(self, cv: tk.Canvas, p_full: float): + """Draw a horizontal occupancy bar on the given canvas.""" cv.delete("all") w = max(300, cv.winfo_width() or 600) h = 18 @@ -622,6 +646,7 @@ class LayoutWindow(ctk.CTkToplevel): # ---------------- UTIL ---------------- def _toast(self, msg, ms=1400): + """Show a transient status message at the bottom of the window.""" if not hasattr(self, "_status"): self._status = ctk.CTkLabel(self, anchor="w") self._status.grid(row=3, column=0, sticky="ew") @@ -629,6 +654,7 @@ class LayoutWindow(ctk.CTkToplevel): self.after(ms, lambda: self._status.configure(text="")) def _copy(self, txt: str): + """Copy a string to the clipboard and inform the user.""" self.clipboard_clear() self.clipboard_append(txt) self._toast(f"Copiato: {txt}") @@ -636,6 +662,7 @@ class LayoutWindow(ctk.CTkToplevel): def destroy(self): + """Mark the window as closed and release dynamic widgets safely.""" # evita nuovi refresh/async dopo destroy self._alive = False # cancella eventuali timer @@ -656,6 +683,7 @@ class LayoutWindow(ctk.CTkToplevel): pass def open_layout_window(parent, db_app): + """Open the layout window as a singleton-like child of ``parent``.""" key = "_layout_window_singleton" ex = getattr(parent, key, None) if ex and ex.winfo_exists(): diff --git a/main.py b/main.py index 615011c..9021149 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,22 @@ +"""Application entry point for the warehouse desktop tool. + +This module wires together the shared async database client, the global +background event loop and the different Tk/CustomTkinter windows exposed by the +project. +""" -import sys import asyncio +import sys 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 async_msssql_query import AsyncMSSQLClient, make_mssql_dsn 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 +from view_celle_multiple import open_celle_multiple_window # Try factory, else frame, else app (senza passare conn_str all'App) try: @@ -18,22 +24,22 @@ try: 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): + """Build the picking list UI using the frame-based fallback.""" 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 + """Fallback used only by legacy app-style picking list implementations.""" + app = _PLApp() app.mainloop() return tk.Frame(parent) - # ---- Config ---- SERVER = r"mde3\gesterp" DBNAME = "Mediseawall" @@ -46,13 +52,16 @@ if sys.platform.startswith("win"): except Exception: pass -# Create ONE global loop and make it the default everywhere +# 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): + """Compatibility no-op used when optional Tk DPI hooks are missing.""" 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"): @@ -63,31 +72,30 @@ db_app = AsyncMSSQLClient(dsn_app) def open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient): + """Open the picking list window while minimizing initial flicker.""" 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 + # Keep the toplevel hidden while its content is being created. 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 + # Show the window only when the layout is ready. try: win.update_idletasks() try: - win.transient(parent) # z-order legato alla main + win.transient(parent) except Exception: pass try: @@ -99,7 +107,6 @@ def open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient): win.focus_force() except Exception: pass - # ripristina opacità try: win.attributes("-alpha", 1.0) except Exception: @@ -112,10 +119,11 @@ def open_pickinglist_window(parent: tk.Misc, db_client: AsyncMSSQLClient): return win - - class Launcher(ctk.CTk): + """Main launcher window that exposes the available warehouse tools.""" + def __init__(self): + """Create the launcher toolbar and wire every button to a feature window.""" super().__init__() self.title("Warehouse 1.0.0") self.geometry("1200x70+0+0") @@ -123,21 +131,37 @@ class Launcher(ctk.CTk): 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") + ctk.CTkButton( + wrap, + text="Gestione Corsie", + command=lambda: open_reset_corsie_window(self, db_app), + ).grid(row=0, column=0, padx=6, pady=6, sticky="ew") + ctk.CTkButton( + wrap, + text="Gestione Layout", + command=lambda: open_layout_window(self, db_app), + ).grid(row=0, column=1, padx=6, pady=6, sticky="ew") + ctk.CTkButton( + wrap, + text="UDC Fantasma", + command=lambda: open_celle_multiple_window(self, db_app), + ).grid(row=0, column=2, padx=6, pady=6, sticky="ew") + ctk.CTkButton( + wrap, + text="Ricerca UDC", + command=lambda: open_search_window(self, db_app), + ).grid(row=0, column=3, padx=6, pady=6, sticky="ew") + ctk.CTkButton( + wrap, + text="Gestione Picking List", + command=lambda: open_pickinglist_window(self, db_app), + ).grid(row=0, column=4, padx=6, pady=6, sticky="ew") for i in range(5): wrap.grid_columnconfigure(i, weight=1) def _on_close(): + """Dispose shared resources before closing the launcher.""" try: fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop) try: diff --git a/prenota_sprenota_sql.py b/prenota_sprenota_sql.py index 5629f2b..cf55357 100644 --- a/prenota_sprenota_sql.py +++ b/prenota_sprenota_sql.py @@ -1,44 +1,42 @@ +"""Async port of the packing list reservation stored procedure.""" + from __future__ import annotations + from dataclasses import dataclass -from typing import Optional, Any, Dict, List +from typing import Any, Dict, List, Optional @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 + """Container returned by the async stored-procedure port.""" + + rc: int = 0 + message: Optional[str] = "" + id_result: Optional[int] = None -# --- 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. - """ + """Return the first column of the first row from a query result.""" 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] + for key in ("rows", "data", "result", "records"): + if key in res and isinstance(res[key], list): + rows = res[key] 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] + row0 = rows[0] + if isinstance(row0, dict): + return next(iter(row0.values()), None) + if isinstance(row0, (list, tuple)) and row0: + return row0[0] return None - # fallback: altri metodi (se esistono) if hasattr(db, "query_value"): return await db.query_value(sql, params) if hasattr(db, "scalar"): @@ -47,7 +45,7 @@ async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]: - """Ritorna una lista di dict {col:val}.""" + """Return all rows as dictionaries, normalizing different DB client APIs.""" if hasattr(db, "query_json"): res = await db.query_json(sql, params) if res is None: @@ -55,64 +53,50 @@ async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any 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] + for key in ("rows", "data", "result", "records"): + if key in res and isinstance(res[key], list): + rows = res[key] 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))) }) + for row in rows: + if isinstance(row, (list, tuple)) and cols: + out.append({(cols[i] if i < len(cols) else f"c{i}"): row[i] for i in range(min(len(cols), len(row)))}) 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. - """ + """Execute a DML statement using the best method exposed by the DB client.""" 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()) + """Toggle the reservation state of all cells belonging to a packing list. + + The implementation mirrors the original SQL stored procedure while using + the shared async DB client already managed by the application. """ 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} + {"IDOperatore": IDOperatore}, ) or "" - # 2) Celle da trattare celle = await _query_all( db, """ @@ -120,18 +104,19 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) - FROM dbo.XMag_ViewPackingList WHERE Documento = :Documento """, - {"Documento": Documento} + {"Documento": Documento}, ) - id_celle = [r.get("Cella") for r in celle if "Cella" in r] + id_celle = [row.get("Cella") for row in celle if "Cella" in row] - # 3) Toggle stato per ogni cella + # Each cell is toggled individually because the original procedure also + # updates metadata such as operator and timestamp per row. for id_cella in id_celle: if id_cella is None: continue stato = await _query_one_value( db, "SELECT IDStato FROM Celle WHERE ID = :IDC", - {"IDC": id_cella} + {"IDC": id_cella}, ) if stato == 0: await _execute( @@ -143,7 +128,7 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) - ModDataOra = GETDATE() WHERE ID = :IDC """, - {"N": nominativo, "IDC": id_cella} + {"N": nominativo, "IDC": id_cella}, ) else: await _execute( @@ -155,10 +140,9 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) - ModDataOra = GETDATE() WHERE ID = :IDC """, - {"N": nominativo, "IDC": id_cella} + {"N": nominativo, "IDC": id_cella}, ) - # 4) Description = NAZIONE (TOP 1) description = await _query_one_value( db, """ @@ -168,22 +152,19 @@ async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) - GROUP BY Documento, NAZIONE ORDER BY NAZIONE """, - {"Documento": Documento} + {"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} + {"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) + except Exception as exc: + return SPResult(rc=-1, message=str(exc), id_result=None) diff --git a/pyproject.toml b/pyproject.toml index a333a0c..1c715f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,11 @@ dependencies = [ [project.optional-dependencies] dev = ["pytest", "pytest-cov", "mypy", "black", "flake8"] +docs = [ + "sphinx>=8.0", + "myst-parser>=4.0", + "sphinxcontrib-mermaid>=1.0", +] [tool.pytest.ini_options] addopts = "-q --maxfail=1" diff --git a/reset_corsie.py b/reset_corsie.py index 3fe0127..fbcff5e 100644 --- a/reset_corsie.py +++ b/reset_corsie.py @@ -1,12 +1,17 @@ -# reset_corsie.py +"""Window used to inspect and empty an entire warehouse aisle. + +The module exposes a destructive maintenance tool: it summarizes the occupancy +state of a selected aisle and, after explicit confirmation, deletes matching +rows from ``MagazziniPallet``. +""" + import tkinter as tk -from tkinter import ttk, messagebox, simpledialog +from tkinter import messagebox, simpledialog, ttk + import customtkinter as ctk -from datetime import datetime -from gestione_aree_frame_async import BusyOverlay, AsyncRunner +from gestione_aree_frame_async import AsyncRunner, BusyOverlay -# ---------------- SQL ---------------- SQL_CORSIE = """ WITH C AS ( SELECT DISTINCT LTRIM(RTRIM(Corsia)) AS Corsia @@ -85,17 +90,14 @@ 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 - """ + """Toplevel used to inspect and clear the pallets assigned to an aisle.""" + def __init__(self, parent, db_client): + """Create the window and immediately load the list of aisles.""" super().__init__(parent) - self.title("Reset Corsie — svuotamento celle per corsia") + self.title("Reset Corsie - svuotamento celle per corsia") self.geometry("1000x680") self.minsize(880, 560) self.resizable(True, True) @@ -107,19 +109,22 @@ class ResetCorsieWindow(ctk.CTkToplevel): self._build_ui() self._load_corsie() - # ---------- UI ---------- def _build_ui(self): - top = ctk.CTkFrame(self); top.pack(fill="x", padx=8, pady=8) + """Create selectors, summary widgets and the occupied-cell grid.""" + 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)) + 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") + 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) + 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 = 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") @@ -133,8 +138,8 @@ class ResetCorsieWindow(ctk.CTkToplevel): 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)) + 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) @@ -143,9 +148,10 @@ class ResetCorsieWindow(ctk.CTkToplevel): 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)) + def _kv(parent_widget, label, var, col): + """Build a compact summary label/value pair.""" + ctk.CTkLabel(parent_widget, text=label, font=("Segoe UI", 9, "bold")).grid(row=0, column=col * 2, sticky="w", padx=(0, 6)) + ctk.CTkLabel(parent_widget, 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) @@ -153,28 +159,30 @@ class ResetCorsieWindow(ctk.CTkToplevel): _kv(g, "Celle doppie:", self.var_dbl, 2) _kv(g, "Tot. pallet:", self.var_pallet, 3) - # ---------- Data ---------- def _load_corsie(self): + """Load available aisles and preselect ``1A`` when present.""" 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…") + + self._async.run(self.db.query_json(SQL_CORSIE, {}), _ok, _err, busy=self._busy, message="Carico corsie...") def refresh(self): + """Refresh both the summary counters and the occupied-cell list.""" 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: @@ -184,39 +192,45 @@ class ResetCorsieWindow(ctk.CTkToplevel): 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") + 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}…") + 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: + for item in self.tree.get_children(): + self.tree.delete(item) + 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): + """Ask for confirmation and start the delete flow for the selected aisle.""" 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:") + msg = ( + f"Verranno cancellati {n} record da MagazziniPallet per la corsia {corsia}.", + "Questa operazione e' irreversibile.", + "Digitare il nome della corsia per confermare:", + ) confirm = simpledialog.askstring("Conferma", "\n".join(msg), parent=self) if confirm is None: return @@ -224,22 +238,27 @@ class ResetCorsieWindow(ctk.CTkToplevel): 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…") + 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): + """Execute the actual delete and refresh the window afterwards.""" 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}…") + 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): + """Create, focus and return the aisle reset window.""" win = ResetCorsieWindow(parent, db_app) - win.lift(); win.focus_set() + win.lift() + win.focus_set() return win diff --git a/search_pallets.py b/search_pallets.py index 3823218..b7f2559 100644 --- a/search_pallets.py +++ b/search_pallets.py @@ -1,28 +1,22 @@ -# 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 +"""Search window for pallets, lots and product codes across the warehouse.""" from __future__ import annotations + import tkinter as tk -from tkinter import ttk, messagebox +from tkinter import filedialog, messagebox, ttk import customtkinter as ctk -from gestione_aree_frame_async import BusyOverlay, AsyncRunner -from tkinter import filedialog +from gestione_aree_frame_async import AsyncRunner, BusyOverlay -# opzionale export xlsx try: from openpyxl import Workbook - from openpyxl.styles import Font, Alignment + from openpyxl.styles import Alignment, Font + _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: @@ -32,17 +26,15 @@ 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 + SELECT b.IDCella, b.UDC, b.Corsia, @@ -55,7 +47,7 @@ JOINED AS ( LEFT JOIN dbo.vXTracciaProdotti AS t ON t.Pallet COLLATE Latin1_General_CI_AS = LEFT(b.UDC, 6) COLLATE Latin1_General_CI_AS ) -SELECT +SELECT j.IDCella, UPPER( CONCAT( @@ -70,18 +62,22 @@ SELECT 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 ( :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 +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): + """Window that searches pallets by barcode, lot or product code.""" + def __init__(self, parent: tk.Widget, db_app): + """Initialize widgets and keep a reference to the shared DB client.""" super().__init__(parent) - self.title("Warehouse · Ricerca UDC/Lotto/Codice") + self.title("Warehouse - Ricerca UDC/Lotto/Codice") self.geometry("1100x720") self.minsize(900, 560) self.resizable(True, True) @@ -89,19 +85,16 @@ class SearchWindow(ctk.CTkToplevel): 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 + """Create the search form, result tree and scrollbars.""" 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): @@ -110,37 +103,27 @@ class SearchWindow(ctk.CTkToplevel): 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.CTkEntry(top, textvariable=self.var_udc, width=160).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.CTkEntry(top, textvariable=self.var_lotto, width=140).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)) + ctk.CTkEntry(top, textvariable=self.var_codice, width=160).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") + ctk.CTkButton(top, text="Cerca", command=self._do_search).grid(row=0, column=6, sticky="w") + ctk.CTkButton(top, text="Esporta XLSX", command=self._export_xlsx).grid(row=0, column=7, sticky="e") - btn_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()) @@ -150,26 +133,23 @@ class SearchWindow(ctk.CTkToplevel): 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("", self._on_dclick) self.tree.bind("", self._maybe_handle_heading_click, add=True) self.tree.bind("", self._on_heading_double_click, add=True) def _apply_zebra(self): + """Reapply alternating row colors and special styling for cell 9999.""" for i, iid in enumerate(self.tree.get_children("")): vals = self.tree.item(iid, "values") zebra = "even" if i % 2 == 0 else "odd" @@ -182,59 +162,59 @@ class SearchWindow(ctk.CTkToplevel): 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")) + """Export the currently visible search results to an Excel file.""" + rows = [self.tree.item(iid, "values") for iid in self.tree.get_children("")] if not rows: messagebox.showinfo("Esporta", "Non ci sono righe da esportare.", parent=self) 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) + 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 + headers = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione") + for j, header in enumerate(headers, start=1): + cell = ws.cell(row=1, column=j, value=header) + cell.font = Font(bold=True) + cell.alignment = Alignment(horizontal="center", vertical="center") + row_idx = 2 for row in rows: - for j, v in enumerate(row, start=1): - ws.cell(row=r, column=j, value=v) - r += 1 - # autosize + for j, value in enumerate(row, start=1): + ws.cell(row=row_idx, column=j, value=value) + row_idx += 1 + 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)) + for j, value in enumerate(row, start=1): + widths[j] = max(widths.get(j, 0), len("" if value is None else str(value))) 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) + + for j, width in widths.items(): + ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60) wb.save(fname) messagebox.showinfo("Esporta", f"File creato:\n{fname}", parent=self) - 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 + """Copy the selected pallet barcode when a result cell is double-clicked.""" region = self.tree.identify("region", evt.x, evt.y) if region != "cell": return @@ -242,28 +222,27 @@ class SearchWindow(ctk.CTkToplevel): if not sel: return vals = self.tree.item(sel, "values") - if len(vals) >= 3 and vals[2]: # UDC + if len(vals) >= 3 and vals[2]: 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 + """Prevent heading clicks from selecting phantom result rows.""" 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 + """Sort by the double-clicked heading.""" region = self.tree.identify("region", evt.x, evt.y) if region != "heading": return - col_id = self.tree.identify_column(evt.x) # es. '#1' + col_id = self.tree.identify_column(evt.x) try: - idx = int(col_id.replace('#','')) - 1 + idx = int(col_id.replace("#", "")) - 1 except Exception: return "break" cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione") @@ -272,75 +251,72 @@ class SearchWindow(ctk.CTkToplevel): return "break" def _sort_key_for_col(self, col: str, val: str): + """Return a stable sort key for the given column value.""" if val is None: - return (1, "") # None in fondo + return (1, "") 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): + """Sort the tree rows in place and update heading arrows.""" 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.append( + { + "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.sort(key=lambda r: self._sort_key_for_col(col, r.get(col)), reverse=rev) + rows.sort(key=lambda row: self._sort_key_for_col(col, row.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: + for index, row in enumerate(rows): + self.tree.move(row["iid"], "", index) + titles = { + "IDCella": "IDCella", + "Ubicazione": "Ubicazione", + "UDC": "UDC / Barcode", + "Lotto": "Lotto", + "Codice": "Codice prodotto", + "Descrizione": "Descrizione prodotto", + } + for key, base in titles.items(): + if key == col: arrow = " ▼" if not rev else " ▲" - self.tree.heading(k, text=base + arrow) + self.tree.heading(key, text=base + arrow) else: - self.tree.heading(k, text=base) + self.tree.heading(key, 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): + """Legacy sorter kept for the optional ``tksheet`` backend.""" try: c = event_dict.get("column") except Exception: return if c is None: return - headers = ["IDCella","Ubicazione","UDC","Lotto","Codice","Descrizione"] + headers = ["IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione"] if not (0 <= c < len(headers)): return colname = headers[c] @@ -348,6 +324,7 @@ class SearchWindow(ctk.CTkToplevel): 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: @@ -359,9 +336,9 @@ class SearchWindow(ctk.CTkToplevel): 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) @@ -371,11 +348,11 @@ class SearchWindow(ctk.CTkToplevel): pass def _do_search(self): + """Run the search query using the currently filled filters.""" 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", @@ -384,7 +361,6 @@ class SearchWindow(ctk.CTkToplevel): ): return - # Parametri: passa NULL se campo vuoto -> i filtri "si spengono" params = { "udc": (udc if udc else None), "lotto": (lotto if lotto else None), @@ -393,24 +369,21 @@ class SearchWindow(ctk.CTkToplevel): 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 + for row in rows: + idc, ubi, udc_v, lot_v, cod_v, desc_v = row 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 + except Exception: 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 + for idx, row in enumerate(rows): + idc, ubi, udc_v, lot_v, cod_v, desc_v = row zebra = "even" if idx % 2 == 0 else "odd" try: is9999 = int(idc) == 9999 @@ -419,7 +392,6 @@ class SearchWindow(ctk.CTkToplevel): 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", @@ -427,7 +399,6 @@ class SearchWindow(ctk.CTkToplevel): parent=self, ) else: - # reset campi se risultato non vuoto self.var_udc.set("") self.var_lotto.set("") self.var_codice.set("") @@ -437,15 +408,18 @@ class SearchWindow(ctk.CTkToplevel): 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…") + self._async.run(self.db.query_json(SQL_SEARCH, params), _ok, _err, busy=self._busy, message="Cerco...") def open_search_window(parent, db_app): + """Open a singleton-like search window tied to the launcher instance.""" key = "_search_window_singleton" ex = getattr(parent, key, None) if ex and ex.winfo_exists(): try: - ex.lift(); ex.focus_force(); return ex + ex.lift() + ex.focus_force() + return ex except Exception: pass w = SearchWindow(parent, db_app) diff --git a/sunto_per_app_codex.txt b/sunto_per_app_codex.txt new file mode 100644 index 0000000..2bff201 --- /dev/null +++ b/sunto_per_app_codex.txt @@ -0,0 +1,9 @@ +Sunto utile per la riapertura: + +abbiamo analizzato C:\devel\python\ware_house +abbiamo deciso di standardizzare l’accesso DB su AsyncMSSQLClient in async_msssql_query.py +abbiamo migrato gestione_pickinglist.py per usare solo il db_client condiviso passato da main.py +abbiamo eliminato db_async_singleton.py +verifica fatta: non restano riferimenti al vecchio singleton DB e i file chiave compilano +possibile passo successivo: unificare anche la duplicazione dell’event loop tra async_loop_singleton.py e gestione_aree_frame_async.py +Se vuoi, nel prossimo messaggio posso anche prepararti un riassunto ancora più corto, tipo “prompt di riavvio” da copiare e incollare quando riapri Codex. \ No newline at end of file diff --git a/async_runner.py b/trash/async_runner.py similarity index 55% rename from async_runner.py rename to trash/async_runner.py index 983a183..04b487c 100644 --- a/async_runner.py +++ b/trash/async_runner.py @@ -1,21 +1,29 @@ -# async_runner.py +"""Minimal bridge between Tkinter and an external asyncio loop.""" + import asyncio from typing import Callable + class AsyncRunner: - """Esegue un awaitable sul loop globale e richiama i callback in Tk via .after.""" + """Run coroutines on a background loop and marshal results back to Tk.""" + def __init__(self, tk_root, loop: asyncio.AbstractEventLoop): + """Store the Tk root widget and the loop used for background work.""" 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…") + def run(self, awaitable, on_ok: Callable, on_err: Callable, busy=None, message: str | None = None): + """Schedule an awaitable and optionally show a busy indicator.""" + 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): + """Poll the future until completion and invoke the proper callback.""" if fut.done(): - if busy: busy.hide() + if busy: + busy.hide() try: res = fut.result() on_ok(res) diff --git a/trash/gestione_pickinglist_lento.py b/trash/gestione_pickinglist_lento.py new file mode 100644 index 0000000..cc6c2b4 --- /dev/null +++ b/trash/gestione_pickinglist_lento.py @@ -0,0 +1,712 @@ +"""Picking list management window. + +The module presents a master/detail UI for packing lists, supports reservation +and unreservation through an async stored-procedure port and keeps rendering +smooth by relying on deferred updates and lightweight progress indicators. +""" + +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 + +# === 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: + """Return a string representation, converting ``None`` to an empty string.""" + return "" if v is None else str(v) + +def _first(d: Dict[str, Any], keys: List[str], default: str = ""): + """Return the first non-empty value found among the provided keys.""" + for k in keys: + if k in d and d[k] not in (None, ""): + return d[k] + return default + +# -------------------- column specs -------------------- +@dataclass +class ColSpec: + """Describe one logical column rendered in a ``ScrollTable``.""" + + 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): + """Create the spinner label attached to the given parent 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 the label widget hosting the spinner animation.""" + return self.lbl + + def start(self, text: str = ""): + """Start the animation and optionally show a short status message.""" + if self._active: + return + self._active = True + self.lbl.configure(text=f"{self.FRAMES[self._i]} {text}".strip()) + self._tick() + + def stop(self): + """Stop the animation and clear the label text.""" + 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): + """Advance the spinner animation frame.""" + 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]): + """Create a fixed-header scrollable table rendered with Tk/CTk widgets.""" + 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("", lambda e: self._sync_header_width()) + self.b_inner.bind("", lambda e: self._on_body_configure()) + + self._build_header() + + def _build_header(self): + """Build the static header row using the configured columns.""" + 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): + """Keep the scroll region aligned with the current body content width.""" + 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): + """React to body resize events by syncing dimensions and header scroll.""" + self._update_body_width() + self._sync_header_width() + + def _sync_header_width(self): + """Mirror the body horizontal scroll position on the header canvas.""" + first, _ = self.b_canvas.xview() + self.h_canvas.xview_moveto(first) + + def _xscroll_both(self, *args): + """Scroll header and body together when the horizontal bar moves.""" + self.h_canvas.xview(*args) + self.b_canvas.xview(*args) + + def _xscroll_set_both(self, first, last): + """Update the header viewport and scrollbar thumb in one place.""" + self.h_canvas.xview_moveto(first) + self.xbar.set(first, last) + + def clear_rows(self): + """Remove all rendered body rows.""" + 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, + ): + """Append one row to the table body.""" + 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: + """State holder for one picking list row and its selection checkbox.""" + + def __init__(self, pl: Dict[str, Any], on_check): + """Bind a picking list payload to a ``BooleanVar`` and callback.""" + self.pl = pl + self.var = ctk.BooleanVar(value=False) + self._callback = on_check + + def is_checked(self) -> bool: + """Return whether the row is currently selected.""" + return self.var.get() + + def set_checked(self, val: bool): + """Programmatically update the checkbox state.""" + self.var.set(val) + + def build_checkbox(self, parent) -> ctk.CTkCheckBox: + """Create the checkbox widget bound to this row model.""" + 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): + """Create the master/detail picking list frame.""" + super().__init__(master) + if db_client is None: + raise ValueError("GestionePickingListFrame richiede un db_client condiviso.") + self.db_client = db_client + 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): + """Build toolbar, master table and detail table.""" + 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): + """Render the placeholder row shown when no document is selected.""" + 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]]): + """Rebuild the master table using the latest query results.""" + 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]: + """Return the currently checked picking list row, if any.""" + 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): + """Handle row selection changes and refresh the detail section.""" + # 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): + """Load or reload the picking list summary table from the database.""" + 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): + """Render the detail table for the currently selected document.""" + 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): + """Reserve the selected picking list.""" + 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): + """Unreserve the selected picking list.""" + 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): + """Placeholder for a future export implementation.""" + messagebox.showinfo("Esporta", "Stub esportazione.") + + +# factory per main +def create_frame(parent, *, db_client=None, conn_str=None) -> 'GestionePickingListFrame': + """Factory used by the launcher to build the picking list frame.""" + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("green") + return GestionePickingListFrame(parent, db_client=db_client) + +# =================== /gestione_pickinglist.py =================== diff --git a/layout_window.py.bak_fix_bc_transparent b/trash/layout_window.py.bak_fix_bc_transparent similarity index 100% rename from layout_window.py.bak_fix_bc_transparent rename to trash/layout_window.py.bak_fix_bc_transparent diff --git a/layout_window.py.bak_perf b/trash/layout_window.py.bak_perf similarity index 100% rename from layout_window.py.bak_perf rename to trash/layout_window.py.bak_perf diff --git a/view_celle_multiple.py b/view_celle_multiple.py index eff88eb..b14c19c 100644 --- a/view_celle_multiple.py +++ b/view_celle_multiple.py @@ -1,16 +1,19 @@ -# view_celle_multiple.py +"""Exploration window for cells containing more than one pallet.""" + import json import tkinter as tk -from tkinter import ttk, messagebox, filedialog -import customtkinter as ctk from datetime import datetime +from tkinter import filedialog, messagebox, ttk +import customtkinter as ctk from openpyxl import Workbook -from openpyxl.styles import Font, Alignment +from openpyxl.styles import Alignment, Font from gestione_aree_frame_async import AsyncRunner + def _json_obj(res): + """Normalize raw DB responses into a dictionary with a ``rows`` key.""" if isinstance(res, str): try: res = json.loads(res) @@ -22,6 +25,7 @@ def _json_obj(res): raise RuntimeError(f"{err}\n{detail}") return res if isinstance(res, dict) else {"rows": res} + UBI_B = ( "UPPER(" " CONCAT(" @@ -128,11 +132,17 @@ FROM unione ORDER BY Ord, Corsia; """ + class CelleMultipleWindow(ctk.CTkToplevel): + """Tree-based explorer for duplicated pallet allocations.""" + def __init__(self, root, db_client, runner: AsyncRunner | None = None): + """Bind the shared DB client and immediately load the tree summary.""" super().__init__(root) - self.title("Celle con più pallet") - self.geometry("1100x700"); self.minsize(900,550); self.resizable(True, True) + self.title("Celle con piu' pallet") + self.geometry("1100x700") + self.minsize(900, 550) + self.resizable(True, True) self.db = db_client self.runner = runner or AsyncRunner(self) @@ -142,205 +152,280 @@ class CelleMultipleWindow(ctk.CTkToplevel): self.refresh_all() def _build_layout(self): + """Create the toolbar, lazy-loaded tree and percentage summary table.""" self.grid_rowconfigure(0, weight=5) self.grid_rowconfigure(1, weight=70) self.grid_rowconfigure(2, weight=25, minsize=160) self.grid_columnconfigure(0, weight=1) - toolbar = ctk.CTkFrame(self); toolbar.grid(row=0, column=0, sticky="nsew") + 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) + frame = ctk.CTkFrame(self) + frame.grid(row=1, column=0, sticky="nsew", padx=6, pady=(0, 6)) + frame.grid_rowconfigure(0, weight=1) + frame.grid_columnconfigure(0, weight=1) + self.tree = ttk.Treeview(frame, columns=("col2", "col3"), show="tree headings", selectmode="browse") + self.tree.heading("#0", text="Nodo") + self.tree.heading("col2", text="Descrizione") + self.tree.heading("col3", text="Lotto") + y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set) - self.tree.grid(row=0, column=0, sticky="nsew"); y.grid(row=0, column=1, sticky="ns"); x.grid(row=1, column=0, sticky="ew") + 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)) + 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) + inner.grid_rowconfigure(0, weight=1) + inner.grid_columnconfigure(0, weight=1) + self.sum_tbl = ttk.Treeview(inner, columns=("Corsia", "TotCelle", "CelleMultiple", "Percentuale"), show="headings") + for key, title, width, anchor in ( + ("Corsia", "Corsia", 100, "center"), + ("TotCelle", "Totale celle", 120, "e"), + ("CelleMultiple", ">1 UDC", 120, "e"), + ("Percentuale", "%", 80, "e"), + ): + self.sum_tbl.heading(key, text=title) + self.sum_tbl.column(key, width=width, anchor=anchor) y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview) x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview) self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set) - self.sum_tbl.grid(row=0, column=0, sticky="nsew"); y2.grid(row=0, column=1, sticky="ns"); x2.grid(row=1, column=0, sticky="ew") + self.sum_tbl.grid(row=0, column=0, sticky="nsew") + y2.grid(row=0, column=1, sticky="ns") + x2.grid(row=1, column=0, sticky="ew") def _bind_events(self): + """Attach lazy-load behavior when nodes are expanded.""" self.tree.bind("<>", self._on_open_node) def refresh_all(self): - self._load_corsie(); self._load_riepilogo() + """Reload both the duplication tree and the summary percentage table.""" + self._load_corsie() + self._load_riepilogo() def _load_corsie(self): + """Load root nodes representing aisles with duplicated cells.""" self.tree.delete(*self.tree.get_children()) - async def _q(db): return await db.query_json(SQL_CORSIE, as_dict_rows=True) + + async def _q(db): + return await db.query_json(SQL_CORSIE, as_dict_rows=True) + self.runner.run(_q(self.db), self._fill_corsie, lambda e: messagebox.showerror("Errore", str(e), parent=self)) def _fill_corsie(self, res): + """Populate root tree nodes after the aisle query completes.""" rows = _json_obj(res).get("rows", []) - for r in rows: - corsia = r.get("Corsia"); - if not corsia: continue + for row in rows: + corsia = row.get("Corsia") + if not corsia: + continue node_id = f"corsia:{corsia}" self.tree.insert("", "end", iid=node_id, text=f"Corsia {corsia}", values=("", ""), open=False, tags=("corsia",)) self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "")) def _on_open_node(self, _evt): + """Lazy-load children when a tree node is expanded.""" sel = self.tree.focus() - if not sel: return + if 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] + 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]) + 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)) + """Query duplicated cells for the selected aisle.""" + async def _q(db): + return await db.query_json(SQL_CELLE_DUP_PER_CORSIA, params={"corsia": corsia}, as_dict_rows=True) + + self.runner.run(_q(self.db), lambda res: self._fill_celle(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self)) def _fill_celle(self, parent_iid, res): + """Populate duplicated-cell nodes under an aisle node.""" rows = _json_obj(res).get("rows", []) if not rows: - self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "")); return - for 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}]" + self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "")) + return + for row in rows: + idc = row["IDCella"] + ubi = row["Ubicazione"] + corsia = row.get("Corsia") + num = row.get("NumUDC", 0) + node_id = f"cella:{idc}" + label = f"{ubi} [x{num}]" if self.tree.exists(node_id): self.tree.item(node_id, text=label, values=(f"IDCella {idc}", "")) else: - self.tree.insert(parent_iid, "end", iid=node_id, text=label, - values=(f"IDCella {idc}", ""), open=False, tags=("cella", f"corsia:{corsia}")) - if not any(ch.endswith("::lazy") for ch in self.tree.get_children(node_id)): + self.tree.insert(parent_iid, "end", iid=node_id, text=label, values=(f"IDCella {idc}", ""), open=False, tags=("cella", f"corsia:{corsia}")) + if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)): self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "")) def _load_pallet_for_cella(self, parent_iid, idcella: int): - 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)) + """Query pallet details for a duplicated cell.""" + async def _q(db): + return await db.query_json(SQL_PALLET_IN_CELLA, params={"idcella": idcella}, as_dict_rows=True) + + self.runner.run(_q(self.db), lambda res: self._fill_pallet(parent_iid, res), lambda e: messagebox.showerror("Errore", str(e), parent=self)) def _fill_pallet(self, parent_iid, res): + """Add pallet leaves under the selected cell node.""" rows = _json_obj(res).get("rows", []) if not rows: - self.tree.insert(parent_iid, "end", text="(nessun pallet)", values=("", "")); return + 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 "" + corsia_tag = next((tag for tag in parent_tags if tag.startswith("corsia:")), None) + corsia_val = corsia_tag.split(":", 1)[1] if corsia_tag else "" cella_ubi = self.tree.item(parent_iid, "text") idcella_txt = self.tree.item(parent_iid, "values")[0] idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None - for r in rows: - pallet = r.get("Pallet", ""); desc = r.get("Descrizione", ""); lotto = r.get("Lotto", "") + for row in rows: + pallet = row.get("Pallet", "") + desc = row.get("Descrizione", "") + lotto = row.get("Lotto", "") leaf_id = f"pallet:{idcella_num}:{pallet}" if self.tree.exists(leaf_id): - self.tree.item(leaf_id, text=str(pallet), values=(desc, lotto)); continue - self.tree.insert(parent_iid, "end", iid=leaf_id, text=str(pallet), - values=(desc, lotto), - tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}")) + 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) + """Load the percentage summary by aisle.""" + async def _q(db): + return await db.query_json(SQL_RIEPILOGO_PERCENTUALI, as_dict_rows=True) + self.runner.run(_q(self.db), self._fill_riepilogo, lambda e: messagebox.showerror("Errore", str(e), parent=self)) def _fill_riepilogo(self, res): + """Refresh the bottom summary table.""" rows = _json_obj(res).get("rows", []) - for 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}")) + for item in self.sum_tbl.get_children(): + self.sum_tbl.delete(item) + for row in rows: + self.sum_tbl.insert( + "", + "end", + values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"), + ) def expand_all(self): + """Expand all aisle roots and trigger lazy loading where needed.""" for iid in self.tree.get_children(""): self.tree.item(iid, open=True) if f"{iid}::lazy" in self.tree.get_children(iid): self.tree.delete(f"{iid}::lazy") - corsia = iid.split(":",1)[1] + corsia = iid.split(":", 1)[1] self._load_celle_for_corsia(iid, corsia) def collapse_all(self): + """Collapse all root nodes in the duplication tree.""" for iid in self.tree.get_children(""): self.tree.item(iid, open=False) def export_to_xlsx(self): + """Export both the detailed tree and the summary table to Excel.""" ts = datetime.now().strftime("%d_%m_%Y_%H-%M") default_name = f"esportazione_celle_udc_multiple_{ts}.xlsx" - fname = filedialog.asksaveasfilename(parent=self, title="Esporta in Excel", - defaultextension=".xlsx", - filetypes=[("Excel Workbook","*.xlsx")], - initialfile=default_name) - if not fname: return + 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_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 + def _hdr(ws, headers): + """Write formatted headers into the given worksheet.""" + for j, header in enumerate(headers, start=1): + cell = ws.cell(row=1, column=j, value=header) + cell.font = Font(bold=True) + cell.alignment = Alignment(horizontal="center", vertical="center") + + _hdr(ws_det, det_headers) + _hdr(ws_sum, sum_headers) + + row_idx = 2 for corsia_node in self.tree.get_children(""): for cella_node in self.tree.get_children(corsia_node): for pallet_node in self.tree.get_children(cella_node): tags = self.tree.item(pallet_node, "tags") or () - if "pallet" not in tags: continue - corsia = next((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:")), "") + if "pallet" not in tags: + continue + corsia = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("corsia:")), "") + ubi = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("ubicazione:")), "") + idcella = next((tag.split(":", 1)[1] for tag in tags if tag.startswith("idcella:")), "") pallet = self.tree.item(pallet_node, "text") desc, lotto = self.tree.item(pallet_node, "values") - for j,v in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1): - ws_det.cell(row=r, column=j, value=v) - r += 1 + for j, value in enumerate([corsia, ubi, idcella, pallet, desc, lotto], start=1): + ws_det.cell(row=row_idx, column=j, value=value) + row_idx += 1 - r2 = 2 + row_idx = 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 + for j, value in enumerate(vals, start=1): + ws_sum.cell(row=row_idx, column=j, value=value) + row_idx += 1 def _autosize(ws): + """Resize worksheet columns based on their longest value.""" widths = {} for row in ws.iter_rows(values_only=True): - for j, 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)) + for j, value in enumerate(row, start=1): + value_s = "" if value is None else str(value) + widths[j] = max(widths.get(j, 0), len(value_s)) from openpyxl.utils import get_column_letter - for j, 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) + for j, width in widths.items(): + ws.column_dimensions[get_column_letter(j)].width = min(max(width + 2, 10), 60) + + _autosize(ws_det) + _autosize(ws_sum) + wb.save(fname) + messagebox.showinfo("Esportazione completata", f"File creato:\n{fname}", parent=self) except Exception as ex: messagebox.showerror("Errore esportazione", str(ex), parent=self) + def open_celle_multiple_window(root: tk.Tk, db_client, runner: AsyncRunner | None = None): - win = CelleMultipleWindow(root, db_client, runner=runner); win.lift(); win.focus_set(); return win + """Create, focus and return the duplicated-cells explorer.""" + win = CelleMultipleWindow(root, db_client, runner=runner) + win.lift() + win.focus_set() + return win diff --git a/ware_house_30_03_2026.zip b/ware_house_30_03_2026.zip new file mode 100644 index 0000000..71792ee Binary files /dev/null and b/ware_house_30_03_2026.zip differ diff --git a/warehouse_sp_python.py b/warehouse_sp_python.py index 22de872..a0e70ce 100644 --- a/warehouse_sp_python.py +++ b/warehouse_sp_python.py @@ -16,6 +16,8 @@ import pyodbc @dataclass class SPResult: + """Standard return container used by the handwritten procedure ports.""" + message: str | None = None id_result: int | None = None