From 5fef5a5e4d157e10661b2d1b475c85149fbc1833 Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 3 Sep 2025 16:00:43 +0200 Subject: [PATCH 1/7] backup ricette --- src/lib/helpers/recipe_manager.py | 5 ++++- src/main.py | 26 +++++++++++++++++++++++++- src/ui/main_window/main_window.ui | 8 +++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py index 6a7fb07..01541ab 100644 --- a/src/lib/helpers/recipe_manager.py +++ b/src/lib/helpers/recipe_manager.py @@ -3,6 +3,7 @@ import csv import locale from datetime import datetime import shutil +import socket from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtWidgets import QFileDialog @@ -431,8 +432,10 @@ def backup_current_recipes(config, logger=None): """ Back up current recipes to a timestamped CSV file in the predefined backup directory. """ + # Get the machine's hostname to use for the directory name + machine_name = socket.gethostname() # Define the backup directory and file name - backup_dir = os.path.join('config', 'csv_import', 'backup_csv') + backup_dir = os.path.join('config', 'csv_import', 'backup_csv', machine_name) timestamp = datetime.now().strftime("%d%m%y%H%M%S") backup_file = f"backup_{timestamp}.csv" backup_path = os.path.join(backup_dir, backup_file) diff --git a/src/main.py b/src/main.py index 633c092..a804098 100644 --- a/src/main.py +++ b/src/main.py @@ -3,6 +3,9 @@ import argparse import faulthandler import logging import os + +from src.lib.helpers.recipe_manager import backup_current_recipes + os.environ['PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION']="python" import platform import signal @@ -77,7 +80,7 @@ try: from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import QApplication, QMessageBox, QInputDialog, QLineEdit import sip - from ui import About, Archive, Login, Main_Window, Test, Users_Management,Logs_Management ,Recipe_Selection, \ + from ui import About, Archive, Login, Main_Window, Test, Users_Management, Logs_Management, Recipes_Management , Recipe_Selection, \ Barcode_Recipe_Selection, LastCommit if "--vision" in sys.argv: @@ -215,6 +218,7 @@ try: self.main_window.barcode_selection_a.triggered.connect(self.set_recipe_mode_barcode) self.main_window.ristampa_etichetta_a.triggered.connect(self.reprint_label) self.main_window.tag_a.triggered.connect(self.tag_write) + self.main_window.recipes_export_a.triggered.connect(self.trigger_recipe_backup) if "pipe_cutter" in self.components.keys(): self.main_window.cut_a.setVisible(True) self.main_window.cut_a.triggered.connect(self.cut_tube) @@ -496,6 +500,26 @@ try: def load_recipe_from_rfid(self, data): self.tag_loaded_recipe = data + def trigger_recipe_backup(self): + """ + This method acts as a bridge to call the imported backup function. + """ + try: + # Use the imported function and pass the required objects + backup_current_recipes(config=self.config, logger=logging) + QMessageBox.information( + self.main_window, + "Esportazione Riuscita", + "Backup delle ricette creato con successo." + ) + except Exception as e: + logging.exception("Error during recipe backup") + QMessageBox.critical( + self.main_window, + "Errore di Esportazione", + f"Si รจ verificato un errore durante il salvataggio: {e}" + ) + if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/ui/main_window/main_window.ui b/src/ui/main_window/main_window.ui index b242a0c..b8257fc 100644 --- a/src/ui/main_window/main_window.ui +++ b/src/ui/main_window/main_window.ui @@ -25,7 +25,7 @@ 0 0 843 - 27 + 21 @@ -55,6 +55,7 @@ + @@ -170,6 +171,11 @@ Ultima Versione Software + + + Esportazione ricette + + From 7f0b9c1d047962cf29aaf908c202df89c7a2ff8c Mon Sep 17 00:00:00 2001 From: Adrian Date: Thu, 4 Sep 2025 15:28:52 +0200 Subject: [PATCH 2/7] backup ricette --- src/lib/helpers/recipe_manager.py | 102 ++++++++++++++----- src/ui/main_window/main_window.ui | 2 +- src/ui/test_leak/test_leak.ui | 157 +++++++++--------------------- 3 files changed, 123 insertions(+), 138 deletions(-) diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py index 01541ab..43db1ae 100644 --- a/src/lib/helpers/recipe_manager.py +++ b/src/lib/helpers/recipe_manager.py @@ -131,7 +131,7 @@ def read_steps(row, config, defaults=None, unsupported_steps=None): "chan_sel": safe_parse(row.get("canale_di_prova_2", defaults["canale_di_prova_2"])), "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno_2", defaults["tempo_svuotamento_esterno"])), "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno_2", defaults["tempo_soffiaggio_esterno"])), - "pid_pressure_correction": safe_parse(row.get("pid_pressure_correction", defaults["pid_pressure_correction_2"])), + "pid_pressure_correction": safe_parse(row.get("pid_pressure_correction_2", defaults["pid_pressure_correction_2"])), "pid_mod_config": safe_parse(row.get("pid_mod_config", defaults["pid_mod_config"])), }, "vision": { @@ -288,7 +288,66 @@ def import_recipes(config, csv_path=None, defaults=None, unsupported_steps=None, logger.error(f"Error importing recipes: {e}") raise + +FIELDNAMES = [ + "codice_ricetta", + "cliente", + "part_number", + "dimensione_lotto_abilitata", + "dimensione_lotto", + "verifica_connettore_abilitata", + "connettore", + "verifica_codice_a_barre_abilitata", + "codice_a_barre", + "verifica_resistenza_connettore_abilitata", + "scala_resistenza", + "r nominale", + "tolleranza_resistenza_pos", + "tolleranza_resistenza_neg", + "avvitatura_abilitata", + "viti", + "prova_tenuta_abilitata", + "tempo_pre_riempimento", + "pressione_pre_riempimento", + "tempo_riempimento", + "tempo_assestamento", + "percentuale_minima_pressione_assestamento", + "percentuale_massima_pressione_assestamento", + "tempo_di_test", + "pressione_di_test_delta_minimo", + "pressione_di_test", + "pressione_di_test_delta_massimo", + "pid_pressure_correction", + "tempo_svuotamento", + "pressione_svuotamento", + "prova_tenuta_abilitata_2", + "tempo_pre_riempimento_2", + "pressione_pre_riempimento_2", + "tempo_riempimento_2", + "tempo_assestamento_2", + "percentuale_minima_pressione_assestamento_2", + "percentuale_massima_pressione_assestamento_2", + "tempo_di_test_2", + "pressione_di_test_delta_minimo_2", + "pressione_di_test_2", + "pressione_di_test_delta_massimo_2", + "pid_pressure_correction_2", + "tempo_svuotamento_2", + "pressione_svuotamento_2", + "test_visione_abilitato", + "ricetta_visione", + "stampa_etichetta_abilitata", + "modello_etichetta", + "labeltxt_1", + "labeltxt_2", + "labeltxt_3", + "labeltxt_4", + "labeltxt_5" +] + + def export_recipes(config, csv_path=None, logger=None): + # ... (QFileDialog logic remains the same) ... if csv_path is None: options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog @@ -315,31 +374,29 @@ def export_recipes(config, csv_path=None, logger=None): barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip() print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip() data = [] - fieldnames = set() # Use a set to avoid duplicates # Wrap database operations in a transaction for consistency with db.atomic(): - # Iterate over all recipes in the database for recipe in Recipes.select(): try: steps = recipe.get_steps_map() - exportable = { - # Base fields + + # Create a dictionary with default values for all fields + exportable = {field: "" for field in FIELDNAMES} + + # Populate the dictionary with recipe data + exportable.update({ recipe_name_field: recipe.name, "cliente": recipe.client, "part_number": recipe.part_number, - } + }) - # Add base fields to the fieldnames - fieldnames.update([recipe_name_field, "cliente", "part_number"]) - - # Check and add steps conditionally + # Conditionally update the dictionary for each step if "connector" in steps: exportable.update({ "verifica_connettore_abilitata": "x", "connettore": steps["connector"].spec["connector"] }) - fieldnames.update(["verifica_connettore_abilitata", "connettore"]) if "resistance" in steps: exportable.update({ @@ -349,22 +406,18 @@ def export_recipes(config, csv_path=None, logger=None): "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"], "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"], }) - fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale", - "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"]) if "barcodes" in steps: exportable.update({ barcode_enable_field: "x", barcode_serial_field: steps["barcodes"].spec["serial"] }) - fieldnames.update([barcode_enable_field, barcode_serial_field]) if "screws" in steps: exportable.update({ "avvitatura_abilitata": "x", "viti": steps["screws"].spec["quantity"] }) - fieldnames.update(["avvitatura_abilitata", "viti"]) if "leak_1" in steps: exportable.update({ @@ -375,8 +428,6 @@ def export_recipes(config, csv_path=None, logger=None): "pressione_di_test": steps["leak_1"].spec["test_pressure"], "pid_pressure_correction": steps["leak_1"].spec["pid_pressure_correction"], }) - fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento", - "tempo_di_test", "pressione_di_test"]) if "leak_2" in steps: exportable.update({ @@ -387,29 +438,30 @@ def export_recipes(config, csv_path=None, logger=None): "pressione_di_test_2": steps["leak_2"].spec["test_pressure"], "pid_pressure_correction": steps["leak_1"].spec["pid_pressure_correction"], }) - fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2", - "tempo_di_test_2", "pressione_di_test_2"]) if "vision" in steps: exportable.update({ "test_visione_abilitato": steps["vision"].spec.get("enabled", ""), "ricetta_visione": steps["vision"].spec["recipe"] }) - fieldnames.update(["test_visione_abilitato", "ricetta_visione"]) if "print" in steps: exportable.update({ "stampa_etichetta_abilitata": "x", - print_template_field: steps["print"].spec["template"], + print_template_field: steps["print"].spec.get("template", ""), + # Add the labeltxt fields here + "labeltxt_1": steps["print"].spec.get("labeltxt_1", ""), + "labeltxt_2": steps["print"].spec.get("labeltxt_2", ""), + "labeltxt_3": steps["print"].spec.get("labeltxt_3", ""), + "labeltxt_4": steps["print"].spec.get("labeltxt_4", ""), + "labeltxt_5": steps["print"].spec.get("labeltxt_5", ""), }) - fieldnames.update(["stampa_etichetta_abilitata", print_template_field]) # Append the exportable row to the data data.append(exportable) except Exception as e: if logger: logger.error(f"Error processing recipe {recipe.name}: {e}") - # Continue with next recipe instead of failing the entire export continue except Exception as e: if logger: @@ -420,8 +472,8 @@ def export_recipes(config, csv_path=None, logger=None): if len(data): if logger: logger.info(f"Exporting recipes to {csv_path}") - with open(csv_path, "w", newline="") as f: - writer = csv.DictWriter(f, fieldnames=list(fieldnames)) + with open(csv_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=FIELDNAMES) writer.writeheader() writer.writerows(data) if logger: diff --git a/src/ui/main_window/main_window.ui b/src/ui/main_window/main_window.ui index b8257fc..c921f56 100644 --- a/src/ui/main_window/main_window.ui +++ b/src/ui/main_window/main_window.ui @@ -173,7 +173,7 @@ - Esportazione ricette + Backup ricette diff --git a/src/ui/test_leak/test_leak.ui b/src/ui/test_leak/test_leak.ui index 78a4358..a91c64a 100644 --- a/src/ui/test_leak/test_leak.ui +++ b/src/ui/test_leak/test_leak.ui @@ -184,15 +184,6 @@ - - - - 0 - 0 - 0 - - - @@ -330,15 +321,6 @@ - - - - 0 - 0 - 0 - - - @@ -476,21 +458,13 @@ - - - - 0 - 0 - 0 - - - 20 + 75 true @@ -654,15 +628,6 @@ - - - - 0 - 0 - 0 - - - @@ -800,15 +765,6 @@ - - - - 0 - 0 - 0 - - - @@ -946,21 +902,13 @@ - - - - 0 - 0 - 0 - - - 20 + 75 true @@ -1130,15 +1078,6 @@ - - - - 0 - 0 - 0 - - - @@ -1276,15 +1215,6 @@ - - - - 0 - 0 - 0 - - - @@ -1422,21 +1352,13 @@ - - - - 0 - 0 - 0 - - - 20 + 75 true @@ -1468,6 +1390,7 @@ 12 + 75 true @@ -1487,6 +1410,7 @@ 12 + 75 true @@ -1534,6 +1458,7 @@ 12 + 75 true @@ -1546,6 +1471,7 @@ 16 + 50 false @@ -1559,6 +1485,7 @@ 16 + 50 false @@ -1575,6 +1502,7 @@ 20 + 50 false @@ -1599,6 +1527,7 @@ border: 1px solid black; 20 + 50 false @@ -1623,6 +1552,7 @@ border: 1px solid black; 20 + 50 false @@ -1641,6 +1571,7 @@ border: 1px solid black; 20 + 50 false @@ -1659,6 +1590,7 @@ border: 1px solid black; 16 + 50 false @@ -1672,6 +1604,7 @@ border: 1px solid black; 16 + 50 false @@ -1688,6 +1621,7 @@ border: 1px solid black; 16 + 50 false @@ -1701,6 +1635,7 @@ border: 1px solid black; 16 + 50 false @@ -1714,6 +1649,7 @@ border: 1px solid black; 16 + 50 false @@ -1730,6 +1666,7 @@ border: 1px solid black; 16 + 50 false @@ -1752,6 +1689,7 @@ border: 1px solid black; 20 + 50 false @@ -1776,6 +1714,7 @@ border: 1px solid black; 20 + 50 false @@ -1800,6 +1739,7 @@ border: 1px solid black; 20 + 50 false @@ -1818,6 +1758,7 @@ border: 1px solid black; 16 + 50 false @@ -1837,6 +1778,7 @@ border: 1px solid black; 16 + 50 false @@ -1872,6 +1814,7 @@ border: 1px solid black; 16 + 50 false @@ -1885,6 +1828,7 @@ border: 1px solid black; 16 + 50 false @@ -1901,6 +1845,7 @@ border: 1px solid black; 16 + 50 false @@ -1923,6 +1868,7 @@ border: 1px solid black; 20 + 50 false @@ -1947,6 +1893,7 @@ border: 1px solid black; 20 + 50 false @@ -1965,6 +1912,7 @@ border: 1px solid black; 20 + 50 false @@ -1983,6 +1931,7 @@ border: 1px solid black; 16 + 50 false @@ -2017,6 +1966,7 @@ border: 1px solid black; 48 + 50 false @@ -2038,6 +1988,7 @@ border: 1px solid black; 16 + 50 false @@ -2054,6 +2005,7 @@ border: 1px solid black; 16 + 50 false @@ -2083,6 +2035,7 @@ border: 1px solid black; 16 + 50 false @@ -2096,6 +2049,7 @@ border: 1px solid black; 16 + 50 false @@ -2109,6 +2063,7 @@ border: 1px solid black; 16 + 50 false @@ -2131,6 +2086,7 @@ border: 1px solid black; 20 + 50 false @@ -2149,6 +2105,7 @@ border: 1px solid black; 16 + 50 false @@ -2171,6 +2128,7 @@ border: 1px solid black; 20 + 50 false @@ -2189,6 +2147,7 @@ border: 1px solid black; 16 + 50 false @@ -2205,6 +2164,7 @@ border: 1px solid black; 16 + 50 false @@ -2218,6 +2178,7 @@ border: 1px solid black; 16 + 50 false @@ -2242,6 +2203,7 @@ border: 1px solid black; 16 + 50 false @@ -2258,6 +2220,7 @@ border: 1px solid black; 16 + 50 false @@ -2271,37 +2234,7 @@ border: 1px solid black; 16 - false - - - - background-color: rgb(255, 255, 255); -border: 1px solid black; - - - - - - - - - - - - - 16 - false - - - - Valore PID - - - - - - - - 16 + 50 false From 53fbeda6d99e314426d831d8b91bb0bc52e66f35 Mon Sep 17 00:00:00 2001 From: edo-neo Date: Thu, 4 Sep 2025 16:20:48 +0200 Subject: [PATCH 3/7] fix issue about name for csv refactoring to use only one file and one directory --- src/lib/helpers/recipe_manager.py | 36 +++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py index 43db1ae..31ddd0d 100644 --- a/src/lib/helpers/recipe_manager.py +++ b/src/lib/helpers/recipe_manager.py @@ -482,20 +482,38 @@ def export_recipes(config, csv_path=None, logger=None): def backup_current_recipes(config, logger=None): """ - Back up current recipes to a timestamped CSV file in the predefined backup directory. + Back up current recipes to a CSV file in a single common folder. The file name equals the + [machine]/description from the active machine config (sanitized). Saving overwrites any + previous backup with the same machine description. """ - # Get the machine's hostname to use for the directory name - machine_name = socket.gethostname() - # Define the backup directory and file name - backup_dir = os.path.join('config', 'csv_import', 'backup_csv', machine_name) - timestamp = datetime.now().strftime("%d%m%y%H%M%S") - backup_file = f"backup_{timestamp}.csv" - backup_path = os.path.join(backup_dir, backup_file) + # Get machine description from config; fall back to hostname if missing + machine_desc = None + try: + machine_desc = (config.get("machine", {}) or {}).get("description") + except Exception: + machine_desc = None + if not machine_desc: + machine_desc = socket.gethostname() + + # Sanitize description to be safe as a file name + safe_name = str(machine_desc).strip() + # Replace path separators and common forbidden characters on Windows/Unix + for ch in ['\\', '/', ':', '*', '?', '"', '<', '>', '|']: + safe_name = safe_name.replace(ch, '_') + # Also collapse consecutive spaces + safe_name = ' '.join(safe_name.split()) + # Ensure .csv extension + if not safe_name.lower().endswith('.csv'): + safe_name = f"{safe_name}.csv" + + # Define the single backup directory and file path + backup_dir = os.path.join('config', 'csv_import', 'backup_csv') + backup_path = os.path.join(backup_dir, safe_name) # Ensure the backup directory exists os.makedirs(backup_dir, exist_ok=True) - # Export current recipes to the backup path + # Export current recipes to the backup path (export_recipes overwrites the file) export_recipes(config=config, csv_path=backup_path, logger=logger) if logger: From 33e133e2d051f670134aa8ddc1588601661ecbb9 Mon Sep 17 00:00:00 2001 From: adri-neo Date: Mon, 8 Sep 2025 15:54:16 +0200 Subject: [PATCH 4/7] polythec --- .../st-ten-13/670054812.svg | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 config/instruction_images/st-ten-13/670054812.svg diff --git a/config/instruction_images/st-ten-13/670054812.svg b/config/instruction_images/st-ten-13/670054812.svg new file mode 100644 index 0000000..3f782fd --- /dev/null +++ b/config/instruction_images/st-ten-13/670054812.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + From b94f7392143c505d349e03a4323ba63ec1b83ae7 Mon Sep 17 00:00:00 2001 From: edo-neo Date: Mon, 15 Sep 2025 08:59:38 +0200 Subject: [PATCH 5/7] st-ten 5 fix for second leak test and instruction extra showing in spec --- config/machine_settings/st-ten-5.ini | 2 +- src/lib/helpers/recipe_manager.py | 3 +-- src/ui/recipe_selection/recipe_selection.py | 15 +++++++++++---- src/ui/test/test.py | 19 ++++++++++++++----- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/config/machine_settings/st-ten-5.ini b/config/machine_settings/st-ten-5.ini index 45fae13..9ad5871 100644 --- a/config/machine_settings/st-ten-5.ini +++ b/config/machine_settings/st-ten-5.ini @@ -66,7 +66,7 @@ istruzione_abilitata: x numero nastri (n):0 numero sensori anello (sa):0 numero sensori presenza (sp):0 - +istruzione_abilitata_extra: prova_tenuta_abilitata: x tempo_pre_riempimento: 0 pressione_pre_riempimento: 5000 diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py index 7d044e0..a7baac0 100644 --- a/src/lib/helpers/recipe_manager.py +++ b/src/lib/helpers/recipe_manager.py @@ -248,8 +248,7 @@ def import_recipes(config, csv_path=None, defaults=None, unsupported_steps=None, "instruction": len( row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in ( unsupported_steps or []), - "instruction_extra": len(row.get("istruzione_abilitata_extra", defaults[ - "istruzione_abilitata_extra"])) and "instruction_extra" not in (unsupported_steps or []), + "instruction_extra": (str(row.get("istruzione_abilitata_extra", defaults["istruzione_abilitata_extra"])) or "").strip().lower() == "x" and "instruction_extra" not in (unsupported_steps or []), "pipe_cutter": len(row.get("tagliatubi", defaults["tagliatubi"])) and "pipe_cutter" not in (unsupported_steps or []), "vision": len( row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in ( diff --git a/src/ui/recipe_selection/recipe_selection.py b/src/ui/recipe_selection/recipe_selection.py index 7553f43..eb8f720 100755 --- a/src/ui/recipe_selection/recipe_selection.py +++ b/src/ui/recipe_selection/recipe_selection.py @@ -37,9 +37,16 @@ class Recipe_Selection(Widget): global noner super().__init__() self.config = config - self.second_leak_test_enabled = self.config["hardware_config"]["second_leak_test"] == "present" + self.second_leak_test_enabled = self.config["hardware_config"].get("second_leak_test", "absent") == "present" self.defaults = self.config.get("recipes_defaults", noner) - self.unsupported_steps = unsupported_steps + self.unsupported_steps = set(unsupported_steps or set()) + # Hide instruction_extra entirely unless explicitly enabled in recipes_defaults (istruzione_abilitata_extra: x) + try: + instr_extra_enabled = str(self.config.get("recipes_defaults", noner)["istruzione_abilitata_extra"]).strip().lower() == "x" + except Exception: + instr_extra_enabled = False + if not instr_extra_enabled: + self.unsupported_steps.add("instruction_extra") session = Users.get_session() if session.is_admin: readonly = False @@ -147,11 +154,11 @@ class Recipe_Selection(Widget): self.config.get("recipes_defaults", noner)["verifica_resistenza_connettore_abilitata"]) and "resistance" not in self.unsupported_steps, "screws": len(self.config.get("recipes_defaults", noner)["avvitatura_abilitata"]) and "screws" not in self.unsupported_steps, "instruction": len(self.config.get("recipes_defaults", noner)["istruzione_abilitata"]) and "instruction" not in self.unsupported_steps, - "instruction_extra": len(self.config.get("recipes_defaults", noner)["istruzione_abilitata_extra"]) and "instruction_extra" not in self.unsupported_steps, + "instruction_extra": (str(self.config.get("recipes_defaults", noner)["istruzione_abilitata_extra"]).strip().lower() == "x") and "instruction_extra" not in self.unsupported_steps, "pipe_cutter": len(self.config.get("recipes_defaults", noner)["tagliatubi_abilitata"]) and "pipe_cutter" not in self.unsupported_steps, "vision": len(self.config.get("recipes_defaults", noner)["test_visione_abilitato"]) and "vision" not in self.unsupported_steps, "leak_1": len(self.config.get("recipes_defaults", noner)["prova_tenuta_abilitata"]) and "leak_1" not in self.unsupported_steps, - "leak_2": len(self.config.get("recipes_defaults", noner)["prova_tenuta_abilitata_2"]) and "leak_2" not in self.unsupported_steps, + "leak_2": (self.second_leak_test_enabled and len(self.config.get("recipes_defaults", noner)["prova_tenuta_abilitata_2"]) and "leak_2" not in self.unsupported_steps), "print": len(self.config.get("recipes_defaults", noner)["stampa_etichetta_abilitata"]) and "print" not in self.unsupported_steps, "step_editors": step_defaults, }, diff --git a/src/ui/test/test.py b/src/ui/test/test.py index 0f77727..600756a 100755 --- a/src/ui/test/test.py +++ b/src/ui/test/test.py @@ -143,6 +143,9 @@ class Test(Widget): # if dependency not in self.components or not self.components[dependency].ready: if dependency not in self.components: self.unsupported_steps.add(step_name) + # Enforce second leak test hardware flag + if self.config["hardware_config"].get("second_leak_test", "absent") != "present": + self.unsupported_steps.add("leak_2") # INIT PIECES COUNTER self.pieces = {"ok": 0, "ko": 0} # INIT CYCLE STATES @@ -164,7 +167,8 @@ class Test(Widget): "leak_1": Test_Assembly(img_path=None, text=None, widget=Test_Leak(config=self.config,components=self.components, recipe=self.recipe, step=self.step, pieces=self.pieces, parent=self)) if self.config["hardware_config"]["tecna_t3"] != "absent" or self.config["hardware_config"]["furness_controls"] !="absent" else None, "leak_2": Test_Assembly(img_path=None, text=None, widget=Test_Leak(config=self.config,components=self.components, recipe=self.recipe, step=self.step, pieces=self.pieces, parent=self)) - if self.config["hardware_config"]["tecna_t3"] != "absent" or self.config["hardware_config"]["furness_controls"] != "absent" else None, + if ((self.config["hardware_config"]["tecna_t3"] != "absent" or self.config["hardware_config"]["furness_controls"] != "absent") + and self.config["hardware_config"].get("second_leak_test", "absent") == "present") else None, "flush": Test_Assembly(img_path=None, text=u"SCARICO ARIA IN CORSO - ATTENDERE...", widget=Test_Warning_Img(components=self.components, recipe=self.recipe, step=self.step)), "instruction": Test_Assembly(img_path=None, text=u"ESEGUIRE LE OPERAZIONI DI MONTAGGIO INDICATE IN FIGURA", widget=Test_Instructions(config=self.config,components=self.components, recipe=self.recipe, bench_name=self.config.machine_id, step=self.step)), @@ -407,8 +411,13 @@ class Test(Widget): self.set_recipe(recipe=None) if self.config["hardware_config"]["tecna_t3"] == "present" or self.config["hardware_config"][ "furness_controls"] == "present": - self.cycle_available_steps["leak_1"].widget.recipe_written = False - self.cycle_available_steps["leak_2"].widget.recipe_written = False + # Reset recipe_written flags for leak widgets if they exist + leak1 = self.cycle_available_steps.get("leak_1") + if leak1 is not None and getattr(leak1, "widget", None) is not None: + leak1.widget.recipe_written = False + leak2 = self.cycle_available_steps.get("leak_2") + if leak2 is not None and getattr(leak2, "widget", None) is not None: + leak2.widget.recipe_written = False self.step = Step(step_type="select_recipe") self.cycle_index = -1 self.recipe = None @@ -633,12 +642,12 @@ class Test(Widget): leak1_index = step_types.index("leak_1") leak2_index = step_types.index("leak_2") if leak1_index + 1 == leak2_index: # Ensure 'leak_1' is immediately followed by 'leak_2' - if self.config["hardware_config"].get("second_leak_test", "yes") == "no": + if recipe and getattr(recipe, 'spec', None) and recipe.spec.get("instruction_extra") and "instruction_extra" not in self.unsupported_steps: steps.insert(leak2_index, Step(step_type="instruction_extra", spec={})) inserted_instruction = True # Insert 'instruction_extra' after the first 'instructions' if not inserted between leaks - if not inserted_instruction: + if not inserted_instruction and recipe and getattr(recipe, 'spec', None) and recipe.spec.get("instruction_extra") and "instruction_extra" not in self.unsupported_steps: for i, step in enumerate(steps): if step.step_type == "instructions": steps.insert(i + 1, Step(step_type="instruction_extra", spec={})) From e537721aca3fb8283df57884fce3a119b2e7fd16 Mon Sep 17 00:00:00 2001 From: edo-neo Date: Mon, 15 Sep 2025 10:34:15 +0200 Subject: [PATCH 6/7] auto back up upon save --- src/lib/helpers/recipe_manager.py | 159 +++++++------------- src/ui/crud/crud.py | 6 + src/ui/recipe_selection/recipe_selection.py | 21 ++- 3 files changed, 77 insertions(+), 109 deletions(-) diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py index 5a64766..9c34261 100644 --- a/src/lib/helpers/recipe_manager.py +++ b/src/lib/helpers/recipe_manager.py @@ -3,7 +3,7 @@ import csv import locale from datetime import datetime import shutil -import socket +import re from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtWidgets import QFileDialog @@ -131,7 +131,7 @@ def read_steps(row, config, defaults=None, unsupported_steps=None): "chan_sel": safe_parse(row.get("canale_di_prova_2", defaults["canale_di_prova_2"])), "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno_2", defaults["tempo_svuotamento_esterno"])), "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno_2", defaults["tempo_soffiaggio_esterno"])), - "pid_pressure_correction": safe_parse(row.get("pid_pressure_correction_2", defaults["pid_pressure_correction_2"])), + "pid_pressure_correction": safe_parse(row.get("pid_pressure_correction", defaults["pid_pressure_correction_2"])), "pid_mod_config": safe_parse(row.get("pid_mod_config", defaults["pid_mod_config"])), }, "vision": { @@ -288,66 +288,7 @@ def import_recipes(config, csv_path=None, defaults=None, unsupported_steps=None, logger.error(f"Error importing recipes: {e}") raise - -FIELDNAMES = [ - "codice_ricetta", - "cliente", - "part_number", - "dimensione_lotto_abilitata", - "dimensione_lotto", - "verifica_connettore_abilitata", - "connettore", - "verifica_codice_a_barre_abilitata", - "codice_a_barre", - "verifica_resistenza_connettore_abilitata", - "scala_resistenza", - "r nominale", - "tolleranza_resistenza_pos", - "tolleranza_resistenza_neg", - "avvitatura_abilitata", - "viti", - "prova_tenuta_abilitata", - "tempo_pre_riempimento", - "pressione_pre_riempimento", - "tempo_riempimento", - "tempo_assestamento", - "percentuale_minima_pressione_assestamento", - "percentuale_massima_pressione_assestamento", - "tempo_di_test", - "pressione_di_test_delta_minimo", - "pressione_di_test", - "pressione_di_test_delta_massimo", - "pid_pressure_correction", - "tempo_svuotamento", - "pressione_svuotamento", - "prova_tenuta_abilitata_2", - "tempo_pre_riempimento_2", - "pressione_pre_riempimento_2", - "tempo_riempimento_2", - "tempo_assestamento_2", - "percentuale_minima_pressione_assestamento_2", - "percentuale_massima_pressione_assestamento_2", - "tempo_di_test_2", - "pressione_di_test_delta_minimo_2", - "pressione_di_test_2", - "pressione_di_test_delta_massimo_2", - "pid_pressure_correction_2", - "tempo_svuotamento_2", - "pressione_svuotamento_2", - "test_visione_abilitato", - "ricetta_visione", - "stampa_etichetta_abilitata", - "modello_etichetta", - "labeltxt_1", - "labeltxt_2", - "labeltxt_3", - "labeltxt_4", - "labeltxt_5" -] - - def export_recipes(config, csv_path=None, logger=None): - # ... (QFileDialog logic remains the same) ... if csv_path is None: options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog @@ -374,29 +315,31 @@ def export_recipes(config, csv_path=None, logger=None): barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip() print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip() data = [] + fieldnames = set() # Use a set to avoid duplicates # Wrap database operations in a transaction for consistency with db.atomic(): + # Iterate over all recipes in the database for recipe in Recipes.select(): try: steps = recipe.get_steps_map() - - # Create a dictionary with default values for all fields - exportable = {field: "" for field in FIELDNAMES} - - # Populate the dictionary with recipe data - exportable.update({ + exportable = { + # Base fields recipe_name_field: recipe.name, "cliente": recipe.client, "part_number": recipe.part_number, - }) + } - # Conditionally update the dictionary for each step + # Add base fields to the fieldnames + fieldnames.update([recipe_name_field, "cliente", "part_number"]) + + # Check and add steps conditionally if "connector" in steps: exportable.update({ "verifica_connettore_abilitata": "x", "connettore": steps["connector"].spec["connector"] }) + fieldnames.update(["verifica_connettore_abilitata", "connettore"]) if "resistance" in steps: exportable.update({ @@ -406,18 +349,22 @@ def export_recipes(config, csv_path=None, logger=None): "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"], "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"], }) + fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale", + "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"]) if "barcodes" in steps: exportable.update({ barcode_enable_field: "x", barcode_serial_field: steps["barcodes"].spec["serial"] }) + fieldnames.update([barcode_enable_field, barcode_serial_field]) if "screws" in steps: exportable.update({ "avvitatura_abilitata": "x", "viti": steps["screws"].spec["quantity"] }) + fieldnames.update(["avvitatura_abilitata", "viti"]) if "leak_1" in steps: exportable.update({ @@ -428,6 +375,8 @@ def export_recipes(config, csv_path=None, logger=None): "pressione_di_test": steps["leak_1"].spec["test_pressure"], "pid_pressure_correction": steps["leak_1"].spec["pid_pressure_correction"], }) + fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento", + "tempo_di_test", "pressione_di_test", "pid_pressure_correction"]) if "leak_2" in steps: exportable.update({ @@ -438,30 +387,29 @@ def export_recipes(config, csv_path=None, logger=None): "pressione_di_test_2": steps["leak_2"].spec["test_pressure"], "pid_pressure_correction": steps["leak_1"].spec["pid_pressure_correction"], }) + fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2", + "tempo_di_test_2", "pressione_di_test_2", "pid_pressure_correction"]) if "vision" in steps: exportable.update({ "test_visione_abilitato": steps["vision"].spec.get("enabled", ""), "ricetta_visione": steps["vision"].spec["recipe"] }) + fieldnames.update(["test_visione_abilitato", "ricetta_visione"]) if "print" in steps: exportable.update({ "stampa_etichetta_abilitata": "x", - print_template_field: steps["print"].spec.get("template", ""), - # Add the labeltxt fields here - "labeltxt_1": steps["print"].spec.get("labeltxt_1", ""), - "labeltxt_2": steps["print"].spec.get("labeltxt_2", ""), - "labeltxt_3": steps["print"].spec.get("labeltxt_3", ""), - "labeltxt_4": steps["print"].spec.get("labeltxt_4", ""), - "labeltxt_5": steps["print"].spec.get("labeltxt_5", ""), + print_template_field: steps["print"].spec["template"], }) + fieldnames.update(["stampa_etichetta_abilitata", print_template_field]) # Append the exportable row to the data data.append(exportable) except Exception as e: if logger: logger.error(f"Error processing recipe {recipe.name}: {e}") + # Continue with next recipe instead of failing the entire export continue except Exception as e: if logger: @@ -472,8 +420,8 @@ def export_recipes(config, csv_path=None, logger=None): if len(data): if logger: logger.info(f"Exporting recipes to {csv_path}") - with open(csv_path, "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=FIELDNAMES) + with open(csv_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(fieldnames)) writer.writeheader() writer.writerows(data) if logger: @@ -482,41 +430,36 @@ def export_recipes(config, csv_path=None, logger=None): def backup_current_recipes(config, logger=None): """ - Back up current recipes to a CSV file in a single common folder. The file name equals the - [machine]/description from the active machine config (sanitized). Saving overwrites any - previous backup with the same machine description. + Back up current recipes to a CSV file named after the current machine description. + Only one backup file is kept (overwritten on each call). """ - # Get machine description from config; fall back to hostname if missing - machine_desc = None - try: - machine_desc = (config.get("machine", {}) or {}).get("description") - except Exception: - machine_desc = None - if not machine_desc: - machine_desc = socket.gethostname() - - # Sanitize description to be safe as a file name - safe_name = str(machine_desc).strip() - # Replace path separators and common forbidden characters on Windows/Unix - for ch in ['\\', '/', ':', '*', '?', '"', '<', '>', '|']: - safe_name = safe_name.replace(ch, '_') - # Also collapse consecutive spaces - safe_name = ' '.join(safe_name.split()) - # Ensure .csv extension - if not safe_name.lower().endswith('.csv'): - safe_name = f"{safe_name}.csv" - - # Define the single backup directory and file path + # Define the backup directory backup_dir = os.path.join('config', 'csv_import', 'backup_csv') - backup_path = os.path.join(backup_dir, safe_name) + + # Read machine description from config + try: + machine_desc = (config.get('machine', {}) or {}).get('description') + if not machine_desc: + # Fallbacks + machine_desc = getattr(config, 'machine_id', None) or 'backup_recipes' + except Exception: + machine_desc = getattr(config, 'machine_id', None) or 'backup_recipes' + + # Sanitize description to create a safe filename + safe_desc = re.sub(r"[^A-Za-z0-9._-]+", "_", str(machine_desc).strip()) + if not safe_desc: + safe_desc = 'backup_recipes' + + # Build backup file path (no timestamp => single rotating file) + backup_file = f"{safe_desc}.csv" + backup_path = os.path.join(backup_dir, backup_file) # Ensure the backup directory exists os.makedirs(backup_dir, exist_ok=True) - # Export current recipes to the backup path (export_recipes overwrites the file) - export_recipes(config=config, csv_path=backup_path, logger=logger) - - if logger: - logger.info(f"Backup created at: {backup_path}") + # Export current recipes to the backup path (overwrites existing file) + # Suppress internal export logs during automatic backup + export_recipes(config=config, csv_path=backup_path, logger=None) + # Do not log here to avoid duplicate messages; caller will handle final log return backup_path # Return the backup path for reference if needed diff --git a/src/ui/crud/crud.py b/src/ui/crud/crud.py index 6606ee9..949cd32 100755 --- a/src/ui/crud/crud.py +++ b/src/ui/crud/crud.py @@ -246,6 +246,7 @@ class Json_External_Dialog_Editor_Cell_Widget(QPushButton, Cell): class Crud(Widget): modified = pyqtSignal(bool) selected = pyqtSignal(object) + committed = pyqtSignal() def __init__(self, table_name, readonly=False, select=None, filters=None, fields_aliases=None, autocomplete=None, sort=None, pagination=250, display_name=None, row_upgrader=None, widget_classes=None, row_filter=None): super().__init__() @@ -549,6 +550,11 @@ class Crud(Widget): # INDEX DATA WITH PK try: self.db.commit(data, deleted_rows=self.deleted_rows) + # Emit committed signal to notify successful save + try: + self.committed.emit() + except Exception: + pass except Exception as e: self.log.exception(traceback.format_exc()) QMessageBox.critical(None, "Errore Salvataggio DB", str(e)) diff --git a/src/ui/recipe_selection/recipe_selection.py b/src/ui/recipe_selection/recipe_selection.py index eb8f720..3db8a57 100755 --- a/src/ui/recipe_selection/recipe_selection.py +++ b/src/ui/recipe_selection/recipe_selection.py @@ -11,7 +11,7 @@ from PyQt5.QtGui import QKeySequence from PyQt5.QtWidgets import QFileDialog, QMessageBox, QShortcut import shutil -from lib.helpers.recipe_manager import export_recipes, import_recipes,recipe_manager_signals +from lib.helpers.recipe_manager import export_recipes, import_recipes, recipe_manager_signals, backup_current_recipes from lib.helpers.step import Step from ui.crud import Crud, Json_External_Dialog_Editor_Cell_Widget from ui.helpers import replace_widget @@ -175,6 +175,11 @@ class Recipe_Selection(Widget): pagination=25, ) replace_widget(self, "crud_w", self.crud) + # Backup recipes automatically on successful save in CRUD + try: + self.crud.committed.connect(self.on_crud_committed) + except Exception: + pass self.crud_modified = None self.selected = None self.select_b.setEnabled(False) @@ -219,6 +224,20 @@ class Recipe_Selection(Widget): recipe_manager_signals.recipes_imported.connect(self.crud.refresh) + def on_crud_committed(self): + """Triggered after successful save (commit) in the CRUD UI: creates a timestamped CSV backup.""" + try: + backup_path = backup_current_recipes(config=self.config, logger=self.log) + try: + self.log.info(f"Backup CSV created: {backup_path}") + except Exception: + pass + except Exception as e: + try: + self.log.exception(f"Failed to create backup CSV after commit: {e}") + except Exception: + pass + def check_modified(self, modified): self.crud_modified = modified self.check(self.crud_modified, self.selected) From 9acbb9530ad45313f888fff7f5e07b5491186bb4 Mon Sep 17 00:00:00 2001 From: edo-neo Date: Mon, 15 Sep 2025 11:23:58 +0200 Subject: [PATCH 7/7] auto back up upon save --- src/lib/db/crud_db.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/lib/db/crud_db.py b/src/lib/db/crud_db.py index 7ab757f..1f6a97b 100755 --- a/src/lib/db/crud_db.py +++ b/src/lib/db/crud_db.py @@ -1,4 +1,6 @@ -from peewee import TextField +import time +import logging +from peewee import TextField, OperationalError from playhouse.shortcuts import model_to_dict from . import db, models_reference @@ -18,6 +20,33 @@ class Crud_DB: for column_name, filter in filters.items(): self.filter(column_name, filter, filter_storage=self.default_filters) + def _execute_with_retry(self, func, max_retries=8, base_delay=0.1): + """Execute a DB operation with retry on transient SQLite 'database is locked' errors.""" + last_exc = None + for attempt in range(max_retries): + try: + return func() + except OperationalError as e: + msg = str(e).lower() + if "database is locked" in msg or "database is busy" in msg: + delay = min(base_delay * (2 ** attempt), 1.5) + try: + logging.getLogger(__name__).warning( + f"SQLite busy/locked, retrying commit (attempt {attempt + 1}/{max_retries}) after {delay:.2f}s" + ) + except Exception: + pass + time.sleep(delay) + last_exc = e + continue + # Not a transient lock error: re-raise immediately + raise + # Exhausted retries + if last_exc is not None: + raise last_exc + # Fallback if no exception captured (should not happen) + return func() + @db.connection_context() @db.atomic() def commit(self, data, deleted_rows=None): @@ -25,7 +54,7 @@ class Crud_DB: if hasattr(self.table_model, "crud_delete"): deleted = self.table_model.crud_delete(deleted_rows) else: - deleted = self.table_model.delete().where(self.table_pk << deleted_rows).execute() + deleted = self._execute_with_retry(lambda: self.table_model.delete().where(self.table_pk << deleted_rows).execute()) if deleted != len(deleted_rows): raise AssertionError(f"deleted {deleted} rows instead of the expected {len(deleted_rows)}") # SQLITE DOES NOT SUPPORT UPDATE, ONLY REPLACE @@ -42,7 +71,7 @@ class Crud_DB: if hasattr(self.table_model, "crud_update"): self.table_model.crud_update(complete_data) else: - self.table_model.insert_many(complete_data).on_conflict_replace().execute() + self._execute_with_retry(lambda: self.table_model.insert_many(complete_data).on_conflict_replace().execute()) def revert(self): self.sorting.clear()