diff --git a/storico_pickinglist.py b/storico_pickinglist.py index aa46185..7490a17 100644 --- a/storico_pickinglist.py +++ b/storico_pickinglist.py @@ -11,6 +11,7 @@ import customtkinter as ctk from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner +from gestione_scarico import move_pallet_async from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value from ui_tables import merge_tags, style_treeview, zebra_tag @@ -159,6 +160,9 @@ class StoricoPickingListWindow(ctk.CTkToplevel): self._async = AsyncRunner(self) self._busy = InlineBusyOverlay(self, self._theme) self.var_documento = tk.StringVar() + self._selected_documento: str | None = None + self._selected_stato_operativo: str = "" + self._detail_rows: list[dict[str, Any]] = [] self.title(versioned_title(loc_text("history.picking.title", catalog=self._locale_catalog, default="Storico Picking List"), __name__)) self.geometry(str(theme_value(self._theme, "window_geometry", "1200x720"))) @@ -182,7 +186,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel): fg_color=theme_color(self._theme, "toolbar_frame_fg_color", ("#d7d7d7", "#3b3b3b")), ) top.grid(row=0, column=0, sticky="ew", padx=8, pady=8) - top.grid_columnconfigure(3, weight=1) + top.grid_columnconfigure(4, weight=1) label_font = theme_font(self._theme, "toolbar_label_font", ("Segoe UI", 10)) entry_font = theme_font(self._theme, "entry_font", ("Segoe UI", 10)) @@ -200,6 +204,18 @@ class StoricoPickingListWindow(ctk.CTkToplevel): ).grid( row=0, column=2, sticky="w" ) + self.btn_ship_residuals = ctk.CTkButton( + top, + text=loc_text( + "history.picking.button.ship_residuals", + catalog=self._locale_catalog, + default="Versa residui in 7G.1.1", + ), + command=self._ship_selected_residuals, + state="disabled", + font=button_font, + ) + self.btn_ship_residuals.grid(row=0, column=3, sticky="w", padx=(12, 0)) self.master_tree = self._make_tree( row=1, @@ -266,12 +282,15 @@ class StoricoPickingListWindow(ctk.CTkToplevel): def _load_master(self) -> None: params = {"documento": str(self.var_documento.get() or "").strip() or None} + previous_documento = self._selected_documento async def _job(): return await self.db_client.query_json(SQL_STORICO_PL, params) def _ok(res): self._fill_master(_rows_to_dicts(res)) + if previous_documento: + self._restore_master_selection(previous_documento) def _err(ex): messagebox.showerror( @@ -299,6 +318,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel): def _fill_master(self, rows: list[dict[str, Any]]) -> None: self.master_tree.delete(*self.master_tree.get_children("")) self.detail_tree.delete(*self.detail_tree.get_children("")) + self._selected_documento = None + self._selected_stato_operativo = "" + self._detail_rows = [] + self._update_residual_button() for index, row in enumerate(rows): stato = str(row.get("StatoOperativo") or "") is_open_with_shipped = ( @@ -335,17 +358,26 @@ class StoricoPickingListWindow(ctk.CTkToplevel): def _on_master_select(self, _event=None) -> None: selected = self.master_tree.selection() if not selected: + self._selected_documento = None + self._selected_stato_operativo = "" + self._detail_rows = [] + self._update_residual_button() return values = self.master_tree.item(selected[0], "values") if not values: return documento = values[0] + self._selected_documento = str(documento) + self._selected_stato_operativo = str(values[7] if len(values) > 7 else "") + self._detail_rows = [] + self._update_residual_button() async def _job(): return await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento}) def _ok(res): self._fill_detail(_rows_to_dicts(res)) + self._update_residual_button() def _err(ex): messagebox.showerror( @@ -372,6 +404,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel): def _fill_detail(self, rows: list[dict[str, Any]]) -> None: self.detail_tree.delete(*self.detail_tree.get_children("")) + self._detail_rows = rows for index, row in enumerate(rows): is_open_shipped = str(row.get("StatoDocumento") or "") == "P" and int(row.get("Cella") or 0) == 9999 done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999 @@ -394,6 +427,117 @@ class StoricoPickingListWindow(ctk.CTkToplevel): tags=merge_tags(zebra_tag(index), tag), ) + def _update_residual_button(self) -> None: + """Enable the bulk shipment button only for closed picking lists with residual UDCs.""" + + enabled = self._selected_stato_operativo == "Chiusa ERP con residui" + try: + self.btn_ship_residuals.configure(state="normal" if enabled else "disabled") + except Exception: + pass + + def _restore_master_selection(self, documento: str) -> None: + """Re-select a document after a reload, when it is still visible.""" + + for iid in self.master_tree.get_children(""): + values = self.master_tree.item(iid, "values") + if values and str(values[0]) == str(documento): + self.master_tree.selection_set(iid) + self.master_tree.focus(iid) + self.master_tree.see(iid) + self._on_master_select() + return + + def _residual_pallets_from_rows(self, rows: list[dict[str, Any]]) -> list[str]: + """Return distinct residual UDCs that are not already in 7G.1.1.""" + + pallets: list[str] = [] + seen: set[str] = set() + for row in rows: + pallet = str(row.get("Pallet") or "").strip() + if not pallet or pallet in seen: + continue + try: + cella = int(row.get("Cella") or 0) + except Exception: + cella = 0 + if cella == 9999: + continue + seen.add(pallet) + pallets.append(pallet) + return pallets + + def _operator_login(self) -> str: + """Return the user recorded on generated warehouse movements.""" + + login = str(getattr(self.session, "login", "") or "").strip() + return login or "warehouse_ui" + + def _ship_selected_residuals(self) -> None: + """Move all residual UDCs of the selected closed PL to the shipment cell 7G.1.1.""" + + if self._selected_stato_operativo != "Chiusa ERP con residui" or not self._selected_documento: + return + estimated = self._residual_pallets_from_rows(self._detail_rows) + count_text = str(len(estimated)) if estimated else "le" + if not messagebox.askyesno( + loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"), + ( + f"Documento {self._selected_documento}\n\n" + f"Verranno versate in 7G.1.1 {count_text} UDC residue della picking list chiusa.\n" + "L'operazione registra i movimenti nello storico UDC.\n\n" + "Procedere?" + ), + parent=self, + ): + return + + documento = self._selected_documento + utente = self._operator_login() + + async def _job(): + detail_res = await self.db_client.query_json(SQL_STORICO_PL_DETAILS, {"documento": documento}) + detail_rows = _rows_to_dicts(detail_res) + pallets = self._residual_pallets_from_rows(detail_rows) + results: list[dict[str, Any]] = [] + for pallet in pallets: + result = await move_pallet_async( + self.db_client, + barcode_pallet=pallet, + target_idcella=9999, + target_barcode_cella="9000000", + utente=utente, + ) + results.append(result) + return {"pallets": pallets, "results": results} + + def _ok(res): + pallets = res.get("pallets", []) if isinstance(res, dict) else [] + results = res.get("results", []) if isinstance(res, dict) else [] + moved = sum(1 for row in results if int(row.get("ok") or 0) == 1) + messagebox.showinfo( + loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"), + f"Documento {documento}\nUDC residue trovate: {len(pallets)}\nUDC versate in 7G.1.1: {moved}", + parent=self, + ) + self._selected_documento = str(documento) + self._load_master() + + def _err(ex): + messagebox.showerror( + loc_text("history.picking.msg.title", catalog=self._locale_catalog, default="Storico Picking List"), + f"Versamento residui fallito:\n{ex}", + parent=self, + ) + + self._async.run( + _job(), + _ok, + _err, + busy=self._busy, + message=f"Verso residui PL {documento} in 7G.1.1...", + ) + def open_storico_pickinglist_window(parent: tk.Misc, db_client, session=None) -> tk.Misc: """Open the picking-list history window.""" diff --git a/version_info.py b/version_info.py index 6bc1149..9e2fb35 100644 --- a/version_info.py +++ b/version_info.py @@ -27,7 +27,7 @@ MODULE_VERSIONS: dict[str, str] = { "prenota_sprenota_sql": "1.0.0", "reset_corsie": "1.0.0", "search_pallets": "1.0.0", - "storico_pickinglist": "1.0.2", + "storico_pickinglist": "1.0.3", "storico_udc": "1.0.0", "tooltips": "1.0.0", "ui_theme": "1.0.0",