671 lines
35 KiB
Python
Executable File
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!")
|