st-ten-1/src/ui/recipe_selection/recipe_selection.py
2026-02-24 15:18:30 +01:00

671 lines
35 KiB
Python
Executable File

import csv
import locale
import os
import sys
import weakref
from glob import glob
from lib.db import Recipes, Users, db
from PyQt5.QtCore import QTimer, pyqtSignal
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, 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
from ui.recipe_spec_and_step_editor import Recipe_Spec_And_Step_Editor
from ui.widget import Widget
from datetime import datetime
class Noner:
def __getitem__(self, key):
return None
noner = Noner()
class Recipe_Selection(Widget):
ok = pyqtSignal(Recipes)
POSIX_BASE_DIR = "/home/gg/PycharmProjects/st-ten-1/config/csv_import/manual_csv_export"
WINDOWS_BASE_DIR = r"C:\Users\gg\PycharmProjects\st-ten-1\config\csv_import\manual_csv_export"
def __init__(self, config, unsupported_steps=None):
global noner
super().__init__()
self.config = config
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 = set(unsupported_steps or set())
# Gate Free Fall feature by hardware_config flag
if self.config.get("hardware_config", {}).get("free_fall", "absent") != "present":
self.unsupported_steps.add("test_freefall_leak")
# 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
crud_aliases = {
"name": "Ricetta",
"client": "Cliente",
"part_number": "N° disegno",
"spec": "Specifica",
"description": "Descrizione",
}
filters = None
else:
readonly = True
crud_aliases = {
"name": "Ricetta",
"client": "Cliente",
"part_number": "N° disegno",
"spec": "Specifica",
"description": "Descrizione",
}
filters = {"archived": False}
step_defaults = self.read_steps(self.config.get("recipes_defaults", noner), noner)
lp_cfg = self.config.get("label_printer", {}) or {}
try:
ris = int(str(lp_cfg.get("risoluzione", "300")).strip())
except Exception:
ris = 300
if ris == 300:
custom_label_folder = f"config/label_templates/{str(self.config.machine_id)}/300/"
standard_label_folder = f"config/label_templates/300/"
else:
custom_label_folder = f"config/label_templates/{str(self.config.machine_id)}/203/"
standard_label_folder = f"config/label_templates/203/"
if os.path.exists(custom_label_folder):
label_folder = custom_label_folder
else:
label_folder = standard_label_folder
# Build both 203 and 300 template lists for dynamic switching in the editor
# Machine-specific overrides if available
custom_203 = f"config/label_templates/{str(self.config.machine_id)}/203/"
custom_300 = f"config/label_templates/{str(self.config.machine_id)}/300/"
std_203 = "config/label_templates/203/"
std_300 = "config/label_templates/300/"
label_folder_203 = custom_203 if os.path.exists(custom_203) else std_203
label_folder_300 = custom_300 if os.path.exists(custom_300) else std_300
templates_203 = sorted(map(os.path.basename, glob(f"{label_folder_203}*.prn")))
templates_300 = sorted(map(os.path.basename, glob(f"{label_folder_300}*.prn")))
# Available printers from both sections (only 'printer' key) and mapping to resolution
lp1 = self.config.get("label_printer", {}) or {}
lp2 = self.config.get("label_printer_2", {}) or {}
lp1_p = lp1.get("printer", "")
lp2_p = lp2.get("printer", "")
printers_list = []
seen = set()
for p in [lp1_p, lp2_p]:
if p and p not in seen:
seen.add(p)
printers_list.append(p)
printers_resolution = {}
if lp1_p:
try:
printers_resolution[lp1_p] = int(str(lp1.get("risoluzione", "300")).strip())
except Exception:
printers_resolution[lp1_p] = 300
if lp2_p:
try:
printers_resolution[lp2_p] = int(str(lp2.get("risoluzione", "300")).strip())
except Exception:
printers_resolution[lp2_p] = 300
step_defaults.update({
"vision": {
# "recipe": sorted(glob("*.ini", root_dir="./config/vision/recipes/")), # only in python3.10
"recipe": sorted(map(os.path.basename, glob("./config/vision/recipes/*.ini"))),
},
"print": {
# "template": sorted(glob("*.prn", root_dir="./config/label_templates/")), # only in python3.10
"template": sorted(map(os.path.basename, glob(f"{label_folder}*.prn"))),
"templates_203": templates_203,
"templates_300": templates_300,
"printer_selection": printers_list,
"printers_resolution": printers_resolution,
},
}),
self.crud = Crud(
"recipes",
display_name="SELEZIONE RICETTA",
readonly=readonly,
select=list(crud_aliases.keys()),
filters=filters,
fields_aliases=crud_aliases,
autocomplete={
"name": self.config.get("recipes_defaults", noner)["codice_ricetta"],
"client": self.config.get("recipes_defaults", noner)["cliente"],
"part_number": self.config.get("recipes_defaults", noner)["part_number"],
"spec": {
"count": len(self.config.get("recipes_defaults", noner)["dimensione_lotto_abilitata"]) and "count" not in self.unsupported_steps,
"connector": len(self.config.get("recipes_defaults", noner)["verifica_connettore_abilitata"]) and "connector" not in self.unsupported_steps,
"barcodes": len(
self.config.get("recipes_defaults", noner)["verifica_codice_a_barre_abilitata"]) and "barcodes" not in self.unsupported_steps,
"resistance": len(
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": (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,
"test_freefall_leak": len((self.config.get("recipes_defaults") or {}).get("prova_pervieta_abilitata", "")) and "test_freefall_leak" 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,
},
"description": self.config.get("recipes_defaults", noner)["descrizione"],
"archived": False,
},
sort={"name": True},
widget_classes={"spec": lambda *args, **kwargs: Json_External_Dialog_Editor_Cell_Widget(
Recipe_Spec_And_Step_Editor,
*args,
**kwargs,
unsupported_steps=self.unsupported_steps
), },
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)
self.select_b.clicked.connect(self.select)
QShortcut(QKeySequence("Return"), self).activated.connect(self.select_b.click)
QShortcut(QKeySequence("Enter"), self).activated.connect(self.select_b.click)
self.crud.modified.connect(self.check_modified)
self.crud.selected.connect(self.check_selected)
self.crud.emit()
self.crud.db_tw.setColumnWidth(0, 200)
self.crud.db_tw.setColumnWidth(1, 200)
self.crud.db_tw.setColumnWidth(2, 200)
self.crud.db_tw.setColumnWidth(3, 200)
self.crud.db_tw.setColumnWidth(4, 200)
self.crud.db_tw.setColumnWidth(5, 400)
if session.is_admin:
self.import_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().import_recipes())
self.export_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().export_recipes())
self.export_selected_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().export_recipes(selected_only=True))
self.delete_all_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().delete_recipes())
else:
self.import_b.setVisible(False)
self.export_b.setVisible(False)
self.export_selected_b.setVisible(False)
self.delete_all_b.setVisible(False)
# TESTING
if self.config.auto_select is not None:
recipe = self.config.auto_select
cn = self.crud.select_index["name"]
self.crud.db_tw.clearSelection()
for rn in range(1, self.crud.db_tw.rowCount()):
if self.crud.db_tw.cellWidget(rn, cn).text() == recipe:
selection = self.crud.db_tw.model().index(rn, cn)
self.crud.db_tw.setCurrentIndex(selection)
break
self.test_timer = QTimer()
self.test_timer.setSingleShot(True)
self.test_timer.timeout.connect(self.select_b.clicked.emit)
self.test_timer.start(500)
# /TESTING
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)
def check_selected(self, selected=None):
if selected is not None and len(selected) == 1:
selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line)
if selected >= 0 and selected < len(self.crud.data_index):
selected = self.crud.data_index[selected]
if selected is not None:
self.selected = selected
else:
self.selected = None
else:
self.selected = None
self.check(self.crud_modified, self.selected)
def check(self, modified, selected):
self.select_b.setEnabled(modified is False and selected is not None)
def refresh(self):
"""Update the UI based on current admin privileges"""
session = Users.get_session()
# Check if user has admin privileges (either permanent or temporary)
# Use session.is_admin instead of checking roles directly to be consistent with user.py
has_admin = session.is_admin
# Update button visibility
self.import_b.setVisible(has_admin)
self.export_b.setVisible(has_admin)
self.export_selected_b.setVisible(has_admin)
self.delete_all_b.setVisible(has_admin)
# Connect or disconnect button handlers
if has_admin:
# Disconnect existing handlers to avoid multiple connections
try:
self.import_b.clicked.disconnect()
except:
pass
try:
self.export_b.clicked.disconnect()
except:
pass
try:
self.export_selected_b.clicked.disconnect()
except:
pass
try:
self.delete_all_b.clicked.disconnect()
except:
pass
# Connect handlers
self.import_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().import_recipes())
self.export_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().export_recipes())
self.export_selected_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().export_recipes(selected_only=True))
self.delete_all_b.clicked.connect(lambda checked, selfi=weakref.ref(self): selfi().delete_recipes())
# Update CRUD readonly status
if has_admin:
self.crud.set_readonly(False)
else:
self.crud.set_readonly(True)
def select(self):
if self.selected is not None:
self.ok.emit(self.crud.db.table_model.get_by_id(self.selected))
def get_def(self, dict, key):
val = dict.get(key, self.defaults[key])
return val if val != "" else self.defaults[key]
# READ RECIPE STEPS FROM CSV ROW
def read_steps(self, row, defaults=None):
if defaults is None:
global noner
defaults = self.config.get("recipes_defaults", noner)
barcode_serial_field = self.config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
warning_image_field = self.config.get("recipe", {}).get("warning_image_field", "warning_img").strip()
decsep = locale.localeconv()["decimal_point"]
rcsv = row.get("r nominale", defaults["r nominale"]).replace(" ", "").replace(",", decsep).replace("Ω", "").replace("?", "")
if rcsv == "":
rcsv = "999"
print_template_field = self.config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
return {
"count": {
"amount": row.get("dimensione_lotto", defaults["dimensione_lotto"]),
"warning_img": row.get(warning_image_field, defaults["warning_img"]),
"require_discard_piece": row.get("richiedi_inserimento_scarto", defaults["richiedi_inserimento_scarto"])
},
"connector": {
"connector": row.get("connettore", defaults["connettore"]),
},
"barcodes": {
"serial": row.get(barcode_serial_field, defaults["codice_a_barre"]),
"n_pieces": row.get("n_componenti") or defaults["n_componenti"],
"barcode_input_2": row.get("barcode_input_2", "-"),
"barcode_input_3": row.get("barcode_input_3", "-"),
"barcode_input_4": row.get("barcode_input_4", "-"),
"barcode_input_5": row.get("barcode_input_5", "-"),
},
"resistance": {
"scale": locale.atof(row.get("scala_resistenza", defaults["scala_resistenza"])),
"expected": locale.atof(rcsv),
"tolerance_pos": locale.atof(self.get_def(row, "tolleranza_resistenza_pos")),
"tolerance_neg": locale.atof(self.get_def(row, "tolleranza_resistenza_neg")),
},
"screws": {
"quantity": row.get("viti", defaults["viti"])
},
"instruction": {},
"pipe_cutter": {
"length": row.get("lunghezza_corrugato", defaults["lunghezza_corrugato"]),
"diameter": row.get("diametro", defaults["diametro"]),
},
"leak_1": {
"pre_filling_time": int(float(row.get("tempo_pre_riempimento", defaults["tempo_pre_riempimento"]))),
"pre_filling_pressure": int(float(row.get("pressione_pre_riempimento", defaults["pressione_pre_riempimento"]))),
"filling_time": int(float(row.get("tempo_riempimento", defaults["tempo_riempimento"]))),
"settling_time": int(float(self.get_def(row, "tempo_assestamento"))),
"settling_pressure_min_percent": int(float(
row.get("percentuale_minima_pressione_assestamento", defaults["percentuale_minima_pressione_assestamento"]))),
"settling_pressure_max_percent": int(float(
row.get("percentuale_massima_pressione_assestamento", defaults["percentuale_massima_pressione_assestamento"]))),
"test_time": int(float(row.get("tempo_di_test", defaults["tempo_di_test"]))),
"test_pressure_qneg": int(float(row.get("pressione_di_test_delta_minimo", defaults["pressione_di_test_delta_minimo"]))),
"test_pressure": int(float(row.get("pressione_di_test", defaults["pressione_di_test"]))),
"test_pressure_qpos": int(float(row.get("pressione_di_test_delta_massimo", defaults["pressione_di_test_delta_massimo"]))),
"flush_time": int(float(row.get("tempo_svuotamento", defaults["tempo_svuotamento"]))),
"flush_pressure": int(float(row.get("pressione_svuotamento", defaults["pressione_svuotamento"]))),
"chan_sel": int(float(row.get("canale_di_prova", defaults["canale_di_prova"]))),
"ext_flush_time": int(float(row.get("tempo_svuotamento_esterno", defaults["tempo_svuotamento_esterno"]))),
"ext_blow_time": int(float(row.get("tempo_soffiaggio_esterno", defaults["tempo_soffiaggio_esterno"]))),
"pid_pressure_correction" : int(float(row.get("pid_pressure_correction", defaults["pid_pressure_correction"]))),
},
"leak_2": {
"pre_filling_time": int(float(row.get("tempo_pre_riempimento_2", defaults["tempo_pre_riempimento_2"]))),
"pre_filling_pressure": int(float(row.get("pressione_pre_riempimento_2", defaults["pressione_pre_riempimento_2"]))),
"filling_time": int(float(row.get("tempo_riempimento_2", defaults["tempo_riempimento_2"]))),
"settling_time": int(float(row.get("tempo_assestamento_2", defaults["tempo_assestamento_2"]))),
"settling_pressure_min_percent": int(float(
row.get("percentuale_minima_pressione_assestamento_2", defaults["percentuale_minima_pressione_assestamento_2"]))),
"settling_pressure_max_percent": int(float(
row.get("percentuale_massima_pressione_assestamento_2", defaults["percentuale_massima_pressione_assestamento_2"]))),
"test_time": int(float(row.get("tempo_di_test_2", defaults["tempo_di_test_2"]))),
"test_pressure_qneg": int(float(row.get("pressione_di_test_delta_minimo_2", defaults["pressione_di_test_delta_minimo_2"]))),
"test_pressure": int(float(row.get("pressione_di_test_2", defaults["pressione_di_test_2"]))),
"test_pressure_qpos": int(float(row.get("pressione_di_test_delta_massimo_2", defaults["pressione_di_test_delta_massimo_2"]))),
"flush_time": int(float(row.get("tempo_svuotamento_2", defaults["tempo_svuotamento_2"]))),
"flush_pressure": int(float(row.get("pressione_svuotamento_2", defaults["pressione_svuotamento_2"]))),
"chan_sel": int(float(row.get("canale_di_prova_2", defaults["canale_di_prova_2"]))),
"ext_flush_time": int(float(row.get("tempo_svuotamento_esterno_2", defaults["tempo_svuotamento_esterno"]))),
"ext_blow_time": int(float(row.get("tempo_soffiaggio_esterno_2", defaults["tempo_soffiaggio_esterno"]))),
"pid_pressure_correction": int(float(row.get("pid_pressure_correction_2", defaults["pid_pressure_correction_2"]))),
},
"vision": {
"recipe": row.get("ricetta_visione", defaults["ricetta_visione"]),
},
"print": {
"template": row.get(print_template_field, defaults["modello_etichetta"]),
"labeltxt_1": row.get("testo_etich_1", ""),
"labeltxt_2": row.get("testo_etich_2", ""),
"labeltxt_3": row.get("testo_etich_3", ""),
"labeltxt_4": row.get("testo_etich_4", ""),
"labeltxt_5": row.get("barcode_input_finelinea", ""),
"extra_label": row.get("etichette_supplementari", ""),
"printer_selection": row.get("printer_selection", self.config.get("label_printer", {}).get("printer", "")),
},
}
# IMPORT RECIPES FROM CSV FILE TO DATABASE
def import_recipes(self, csv_path=None, defaults=None):
import_recipes(
config=self.config,
csv_path=csv_path,
defaults=defaults,
unsupported_steps=self.unsupported_steps,
logger=self.log,
)
self.crud.refresh()
# EXPORT RECIPES TABLE TO CSV FILE
def export_recipes(self, csv_path=None, base_dir=None, selected_only=False):
"""
Export recipes to a CSV file. Opens a dialog for saving if no path is provided.
Args:
csv_path (str, optional): Path to save the CSV file. If None, a dialog will open.
base_dir (str, optional): Base directory for saving the file. If None, a default is used.
selected_only (bool, optional): If True, export only selected recipes. Default is False.
"""
# Check if we should export only selected recipes
if selected_only:
# Get selected rows
selected_rows = self.crud.get_selected_rows()
if not selected_rows:
QMessageBox.information(self, "Esportazione ricette", "Nessuna ricetta selezionata.")
return
# Ask for confirmation
ret = QMessageBox.question(
self,
"Esportazione ricette selezionate",
f"Vuoi esportare solo le {len(selected_rows)} ricette selezionate?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if ret == QMessageBox.No:
# User chose not to export only selected recipes
selected_only = False
base_dir = base_dir or self._get_base_dir() # Use base directory or default
if csv_path is None:
# Prepare initial directory and filename
initial_path = self._prepare_initial_file_path(base_dir)
# Open Save File dialog with the default path
csv_path, _ = QFileDialog.getSaveFileName(
self,
"Esportazione ricette",
initial_path,
"CSV data (*.csv);;All Files (*)",
)
if not csv_path: # If no file selected, exit
return
# Ensure filename ends with .csv
if not csv_path.lower().endswith(".csv"):
csv_path += ".csv"
# Create directories if needed
output_dir = os.path.dirname(csv_path)
try:
os.makedirs(output_dir, exist_ok=True)
except Exception as e:
self.log.error(f"Error creating directories for export: {e}")
return
recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
barcode_serial_field = self.config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
print_template_field = self.config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
data = []
fieldnames = [
recipe_name_field,
"cliente",
"part_number",
"dimensione_lotto_abilitata",
"dimensione_lotto",
"verifica_connettore_abilitata",
"connettore",
barcode_enable_field,
barcode_serial_field,
"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",
"tempo_svuotamento",
"pressione_svuotamento",
"prova_pervieta_abilitata",
"tempo_riempimento_free_fall",
"pressione_riempimento_free_fall",
"pressione_min_free_fall",
"pressione_max_free_fall",
"riempimento_continuo_free_fall",
"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",
"tempo_svuotamento_2",
"pressione_svuotamento_2",
"test_visione_abilitato",
"ricetta_visione",
"stampa_etichetta_abilitata",
print_template_field,
"labeltxt_1",
"labeltxt_2",
"labeltxt_3",
"labeltxt_4",
"labeltxt_5",
"printer_selection",
]
# Get the selected recipe IDs if we're exporting only selected recipes
selected_recipe_ids = []
if selected_only:
selected_rows = self.crud.get_selected_rows()
for row in selected_rows:
# Adjust row index to match data_index (row - 1 because row 0 is the filter row)
row_idx = row - 1
if 0 <= row_idx < len(self.crud.data_index):
recipe_id = self.crud.data_index[row_idx]
if recipe_id is not None:
selected_recipe_ids.append(recipe_id)
# Get all recipes or only selected ones
recipes_query = Recipes.select()
if selected_only and selected_recipe_ids:
# Recipes model uses 'name' as primary key (no 'id' column). Filter by the PK.
recipes_query = recipes_query.where(Recipes._meta.primary_key.in_(selected_recipe_ids))
for recipe in list(recipes_query):
steps = recipe.get_steps_map()
exportable = {
recipe_name_field: recipe.name,
"cliente": recipe.client,
"part_number": recipe.part_number,
"verifica_connettore_abilitata": "x" if recipe.spec.get("connector",False) else "",
"connettore": steps.get("connector",Step()).spec.get("connector",""),
barcode_enable_field: "x" if recipe.spec.get("barcodes",False) in ("x",True,"true","True") else "",
barcode_serial_field: steps.get("barcodes",Step()).spec.get("serial",""),
"verifica_resistenza_connettore_abilitata": "x" if recipe.spec.get("resistance",False) else "",
"scala_resistenza": steps.get("resistance",Step()).spec.get("scale",""),
"r nominale": steps.get("resistance",Step()).spec.get("expected",""),
"tolleranza_resistenza_pos": steps.get("resistance",Step()).spec.get("tolerance_pos",""),
"tolleranza_resistenza_neg": steps.get("resistance",Step()).spec.get("tolerance_neg",""),
# "avvitatura_abilitata": "x" if recipe.spec.get("screws",False) else "",
# "viti": steps.get("screws",Step()).spec.get("quantity",""),
"prova_tenuta_abilitata": "x" if recipe.spec.get("leak_1",False) else "",
"tempo_pre_riempimento": steps.get("leak_1",Step()).spec.get("pre_filling_time",""),
"pressione_pre_riempimento": steps.get("leak_1",Step()).spec.get("pre_filling_pressure",""),
"tempo_riempimento": steps.get("leak_1",Step()).spec.get("filling_time",""),
"tempo_assestamento": steps.get("leak_1",Step()).spec.get("settling_time",""),
"percentuale_minima_pressione_assestamento": steps.get("leak_1",Step()).spec.get("settling_pressure_min_percent",""),
"percentuale_massima_pressione_assestamento": steps.get("leak_1",Step()).spec.get("settling_pressure_max_percent",""),
"tempo_di_test": steps.get("leak_1",Step()).spec.get("test_time",""),
"pressione_di_test_delta_minimo": steps.get("leak_1",Step()).spec.get("test_pressure_qneg",""),
"pressione_di_test": steps.get("leak_1",Step()).spec.get("test_pressure",""),
"pressione_di_test_delta_massimo": steps.get("leak_1",Step()).spec.get("test_pressure_qpos",""),
"tempo_svuotamento": steps.get("leak_1",Step()).spec.get("flush_time",""),
"pressione_svuotamento": steps.get("leak_1",Step()).spec.get("flush_pressure",""),
"prova_tenuta_abilitata_2": "x" if recipe.spec.get("leak_2", False) in ("x",True,"true","True") else "",
"tempo_pre_riempimento_2": steps.get("leak_2",Step()).spec.get("pre_filling_time",""),
"pressione_pre_riempimento_2": steps.get("leak_2",Step()).spec.get("pre_filling_pressure",""),
"tempo_riempimento_2": steps.get("leak_2",Step()).spec.get("filling_time",""),
"tempo_assestamento_2": steps.get("leak_2",Step()).spec.get("settling_time",""),
"percentuale_minima_pressione_assestamento_2": steps.get("leak_2",Step()).spec.get("settling_pressure_min_percent",""),
"percentuale_massima_pressione_assestamento_2": steps.get("leak_2",Step()).spec.get("settling_pressure_max_percent",""),
"tempo_di_test_2": steps.get("leak_2",Step()).spec.get("test_time",""),
"pressione_di_test_delta_minimo_2": steps.get("leak_2",Step()).spec.get("test_pressure_qneg",""),
"pressione_di_test_2": steps.get("leak_2",Step()).spec.get("test_pressure",""),
"pressione_di_test_delta_massimo_2": steps.get("leak_2",Step()).spec.get("test_pressure_qpos",""),
"tempo_svuotamento_2": steps.get("leak_2",Step()).spec.get("flush_time",""),
"pressione_svuotamento_2": steps.get("leak_2",Step()).spec.get("flush_pressure",""),
"prova_pervieta_abilitata": "x" if recipe.spec.get("test_freefall_leak", False) in ("x",True,"true","True") else "",
"tempo_riempimento_free_fall": steps.get("test_freefall_leak", Step()).spec.get("filling_time", ""),
"pressione_riempimento_free_fall": steps.get("test_freefall_leak", Step()).spec.get("pre_filling_pressure", ""),
"pressione_min_free_fall": steps.get("test_freefall_leak", Step()).spec.get("pressure_min", ""),
"pressione_max_free_fall": steps.get("test_freefall_leak", Step()).spec.get("pressure_max", ""),
"riempimento_continuo_free_fall": "x" if steps.get("test_freefall_leak", Step()).spec.get("continuous_filling", False) in ("x",True,"true","True") else "",
"test_visione_abilitato": recipe.spec.get("vision",""),
"ricetta_visione": steps.get("vision",Step()).spec.get("recipe",""),
"stampa_etichetta_abilitata": "x" if recipe.spec.get("print",False) else "",
print_template_field: steps.get("print",Step()).spec.get("template",""),
"labeltxt_1": steps.get("print",Step()).spec.get("labeltxt_1",""),
"labeltxt_2": steps.get("print", Step()).spec.get("labeltxt_2", ""),
"labeltxt_3": steps.get("print", Step()).spec.get("labeltxt_3", ""),
"labeltxt_4": steps.get("print", Step()).spec.get("labeltxt_4", ""),
"labeltxt_5": steps.get("print", Step()).spec.get("labeltxt_5", ""),
"printer_selection": steps.get("print",Step()).spec.get("printer_selection",""),
}
data.append(exportable)
if len(data):
self.log.info(f"recipes: exporting recipes to {csv_path}")
with open(csv_path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames, extrasaction="ignore")
w.writeheader()
w.writerows(data)
self.log.info(f"recipes: exported {len(data)} rows.")
def delete_recipes(self):
ret = QMessageBox.warning(
None,
"Attenzione si sta cercando di cancellare tutte le ricette!",
"Si è sicuri di voler eliminare tutte le ricette?\nQuesta operazione non può essere annullata!",
buttons=QMessageBox.Ok | QMessageBox.Cancel,
defaultButton=QMessageBox.Cancel
)
if ret == QMessageBox.Ok:
Recipes.delete().execute()
self.crud.refresh()
def _prepare_initial_file_path(self, base_dir):
"""
Prepare the initial file path for the QFileDialog.
Includes a directory and default filename.
"""
timestr = datetime.now().strftime("%d-%m-%Y")
default_filename = f"{self.config.machine_id}_RECIPES_{timestr}.csv"
return os.path.join(base_dir, default_filename)
def _get_base_dir(self):
"""
Returns the base directory for CSV exports based on the operating system.
:raises: Exception if the OS is unsupported.
"""
if os.name == "posix": # For Linux/Unix-based systems
return self.POSIX_BASE_DIR
elif os.name == "nt": # For Windows
return self.WINDOWS_BASE_DIR
else:
raise Exception("Unsupported operating system!")