Topic: script edition nomemclature
Voici un script python que j'ai creer pour éditer les propriétés des éléments dans un schema afin de pouvoir saisir directement le fabricant article description commentaire etc...
You are not logged in. Please login or register.
QElectroTech → Scripts → script edition nomemclature
Voici un script python que j'ai creer pour éditer les propriétés des éléments dans un schema afin de pouvoir saisir directement le fabricant article description commentaire etc...
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
from tkinter import ttk
import xml.etree.ElementTree as ET
from pathlib import Path
import sys
import re
import csv
from datetime import datetime
try:
from tksheet import Sheet
except ImportError:
messagebox.showerror("Erreur", "La librairie 'tksheet' est requise.\n\nExécutez cette commande :\npython -m pip install tksheet")
sys.exit(1)
class QETAnalyzer:
def __init__(self, root):
self.root = root
self.root.title("QElectroTech Nomenclature - Analyseur")
self.root.geometry("1200x700")
self.tree_file = None
self.file_path = None
self.elements_data = []
self.columns_hidden = False # État d'affichage des colonnes
# Définir l'ordre des colonnes une seule fois
self.columns_order = ('UUID', 'LABEL', 'FABRICANT', 'NUM ARTICLE', 'DESCRIPTION', 'COMMENTAIRE',
'LOCALISATION', 'INSTALLATION', 'BLOC AUX 1', 'BLOC AUX 2', 'FONCTION',
'NUMERO DE COMMANDE', 'FOURNISSEUR', 'NUMERO INTERNE', 'UNITE', 'QUANTITE', 'FOLIO')
# Style
style = ttk.Style()
style.theme_use('clam')
# Frame principal
main_frame = ttk.Frame(root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Frame du haut avec boutons
top_frame = ttk.Frame(main_frame)
top_frame.pack(fill=tk.X, pady=5)
ttk.Button(top_frame, text="Ouvrir fichier .qet", command=self.open_file).pack(side=tk.LEFT, padx=5)
ttk.Button(top_frame, text="Sauvegarder", command=self.save_file).pack(side=tk.LEFT, padx=5)
ttk.Button(top_frame, text="Annuler (Undo)", command=lambda: self.sheet.undo()).pack(side=tk.LEFT, padx=5)
ttk.Button(top_frame, text="Rétablir (Redo)", command=lambda: self.sheet.redo()).pack(side=tk.LEFT, padx=5)
ttk.Button(top_frame, text="Exporter en CSV", command=self.export_csv).pack(side=tk.LEFT, padx=5)
ttk.Button(top_frame, text="Masquer/Afficher colonnes", command=self.toggle_columns).pack(side=tk.LEFT, padx=5)
self.file_label = ttk.Label(top_frame, text="Aucun fichier ouvert", foreground="gray")
self.file_label.pack(side=tk.LEFT, padx=20)
# Frame pour le tableau
tree_frame = ttk.Frame(main_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# TKSHEET
self.sheet = Sheet(tree_frame,
headers=list(self.columns_order),
empty_horizontal=0,
empty_vertical=0)
self.sheet.enable_bindings("all") # Active copier/coller, édition, tri, etc.
# Interdire l'ajout/suppression de lignes/colonnes via le clic droit pour éviter les erreurs
self.sheet.disable_bindings("rc_insert_column", "rc_delete_column", "rc_insert_row", "rc_delete_row")
# Rendre les colonnes LABEL (index 1) et FOLIO (index 16) en lecture seule
self.sheet.readonly_columns(columns=[1, 16])
self.sheet.pack(fill="both", expand=True)
# Frame du bas avec info
bottom_frame = ttk.Frame(main_frame)
bottom_frame.pack(fill=tk.X, pady=5)
self.info_label = ttk.Label(bottom_frame, text="Éléments: 0")
self.info_label.pack(side=tk.LEFT)
def open_file(self):
file_path = filedialog.askopenfilename(
title="Sélectionner un fichier QElectroTech",
filetypes=[("QET files", "*.qet"), ("All files", "*.*")]
)
if not file_path:
return
try:
self.file_path = file_path
self.tree_file = ET.parse(file_path)
self.load_elements()
self.file_label.config(text=f"Fichier: {Path(file_path).name}", foreground="green")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de l'ouverture du fichier:\n{str(e)}")
def load_elements(self):
"""Charger les éléments du fichier QET"""
# Réinitialiser l'état d'affichage pour que la vue par défaut (masquée) s'applique
self.columns_hidden = False
self.elements_data = []
if not self.tree_file:
return
root = self.tree_file.getroot()
# Chercher tous les éléments dans le diagramme
for diagram in root.findall('.//diagram'):
# Extraire le numéro de folio depuis l'attribut folio et le titre du diagramme
folio_title = diagram.get('title', '')
folio_attr = diagram.get('folio', '')
folio_number = self.extract_folio_number(folio_title, folio_attr)
for element in diagram.findall('.//element'):
# Ne garder que les éléments qui ont des elementInformations
if element.find('.//elementInformations') is None:
continue
# Filtrer les report de folio (coming_arrow et going_arrow)
element_type = element.get('type', '')
if '01coming_arrow.elmt' in element_type or '02going_arrow.elmt' in element_type:
continue
# Filtrer les éléments avec une seule terminaison
terminals = element.findall('.//terminal')
if len(terminals) <= 1:
# Vérifier si le label commence par 'W' (câbles)
element_info = self.extract_element_info(element)
if not element_info['label'] or not element_info['label'].strip().upper().startswith('W'):
continue
element_info = self.extract_element_info(element)
# Ne garder que les éléments avec un label contenant au moins une lettre (A-Z)
label = element_info['label'].strip()
if not label or not any(c.isalpha() for c in label):
continue
self.elements_data.append({
'element': element,
'uuid': element_info['uuid'],
'label': element_info['label'],
'quantity': element_info['quantity'],
'function': element_info['function'],
'designation': element_info['designation'],
'description': element_info['description'],
'comment': element_info['comment'],
'location': element_info['location'],
'manufacturer': element_info['manufacturer'],
'supplier': element_info['supplier'],
'manufacturer_reference': element_info['manufacturer_reference'],
'plant': element_info['plant'],
'machine_manufacturer_reference': element_info['machine_manufacturer_reference'],
'auxiliary1': element_info['auxiliary1'],
'auxiliary2': element_info['auxiliary2'],
'unity': element_info['unity'],
'folio': folio_number
})
# Tri par défaut sur le label
self.elements_data.sort(key=lambda x: x['label'])
self.populate_sheet()
self.info_label.config(text=f"Éléments affichés: {len(self.elements_data)}")
# Appeler la fonction pour masquer les colonnes optionnelles par défaut
self.toggle_columns()
def populate_sheet(self):
"""Remplir le tableau avec les données actuelles"""
sheet_data = []
for item in self.elements_data:
row = [
item.get('uuid', ''),
item.get('label', ''),
item.get('manufacturer', ''),
item.get('designation', ''),
item.get('description', ''),
item.get('comment', ''),
item.get('location', ''),
item.get('plant', ''),
item.get('auxiliary1', ''),
item.get('auxiliary2', ''),
item.get('function', ''),
item.get('manufacturer_reference', ''),
item.get('supplier', ''),
item.get('machine_manufacturer_reference', ''),
item.get('unity', ''),
item.get('quantity', ''),
item.get('folio', '')
]
sheet_data.append(row)
self.sheet.set_sheet_data(sheet_data)
# Masquer la colonne UUID (index 0)
self.sheet.hide_columns([0])
self.auto_resize_columns()
def auto_resize_columns(self):
"""Redimensionner les colonnes en fonction du contenu"""
sheet_data = self.sheet.get_sheet_data()
# Si pas de données, on arrête là pour éviter les erreurs d'index
if not sheet_data:
return
try:
# Largeur en caractères pour chaque colonne (minimum: largeur de l'en-tête)
col_char_widths = {i: len(str(h)) for i, h in enumerate(self.columns_order)}
# Calculer la largeur maximale en caractères nécessaire pour chaque colonne
for row in sheet_data:
for i, cell in enumerate(row):
col_char_widths[i] = max(col_char_widths.get(i, 0), len(str(cell)))
# Index des colonnes spéciales
desc_idx = self.columns_order.index('DESCRIPTION')
comment_idx = self.columns_order.index('COMMENTAIRE')
# Appliquer les largeurs calculées en pixels
for i, width_in_chars in col_char_widths.items():
pixel_width = width_in_chars * 7 + 20 # Approximation: 7px par caractère + 20px de marge
if i in [desc_idx, comment_idx]:
final_width = min(pixel_width, 500) # Cap à 500px pour les colonnes longues
else:
final_width = min(pixel_width, 300) # Cap à 300px pour les autres
self.sheet.column_width(column=i, width=max(final_width, 40)) # Largeur min de 40px
except (ValueError, AttributeError, IndexError):
# En cas d'erreur (colonne non trouvée ou méthode tksheet inexistante), on ne fait rien
pass
def extract_element_info(self, element):
"""Extraire les informations d'un élément"""
uuid = element.get('uuid', 'N/A')
element_type = element.get('type', 'N/A')
prefix = element.get('prefix', '')
# Initialiser tous les champs
label = ''
quantity = ''
function = ''
designation = ''
description = ''
comment = ''
location = ''
manufacturer = ''
supplier = ''
manufacturer_reference = ''
plant = ''
machine_manufacturer_reference = ''
auxiliary1 = ''
auxiliary2 = ''
unity = ''
elem_infos = element.find('.//elementInformations')
if elem_infos is not None:
for info in elem_infos.findall('elementInformation'):
name = info.get('name', '')
text = info.text or ''
if name == 'label':
label = text
elif name == 'quantity':
quantity = text
elif name == 'function':
function = text
elif name == 'designation':
designation = text
elif name == 'description':
description = text
elif name == 'comment':
comment = text
elif name == 'location':
location = text
elif name == 'manufacturer':
manufacturer = text
elif name == 'supplier':
supplier = text
elif name == 'manufacturer_reference':
manufacturer_reference = text
elif name == 'plant':
plant = text
elif name == 'machine_manufacturer_reference':
machine_manufacturer_reference = text
elif name == 'auxiliary1':
auxiliary1 = text
elif name == 'auxiliary2':
auxiliary2 = text
elif name == 'unity':
unity = text
# Sinon chercher dans dynamic_texts pour le label
if not label:
dynamic_texts = element.find('.//dynamic_texts')
if dynamic_texts is not None:
for text_elem in dynamic_texts.findall('.//dynamic_text'):
# Vérifier si c'est le label
info_name = text_elem.find('info_name')
if info_name is not None and info_name.text == 'label':
text_content = text_elem.find('text')
if text_content is not None and text_content.text:
label = text_content.text
break
return {
'uuid': uuid,
'type': element_type.split('/')[-1] if '/' in element_type else element_type,
'prefix': prefix,
'label': label,
'quantity': quantity,
'function': function,
'designation': designation,
'description': description,
'comment': comment,
'location': location,
'manufacturer': manufacturer,
'supplier': supplier,
'manufacturer_reference': manufacturer_reference,
'plant': plant,
'machine_manufacturer_reference': machine_manufacturer_reference,
'auxiliary1': auxiliary1,
'auxiliary2': auxiliary2,
'unity': unity
}
def extract_folio_number(self, folio_title, folio_attr):
"""Extraire le numéro de folio depuis l'attribut folio ou le titre du diagramme"""
# Priorité à l'attribut folio qui contient toujours le numéro
if folio_attr and folio_attr.strip():
return folio_attr.strip()
# Sinon chercher un numéro dans le titre (fallback)
if not folio_title:
return ''
# Chercher un numéro dans le titre (ex: "Folio 01", "Page 1", etc.)
match = re.search(r'(\d+)', folio_title)
if match:
return match.group(1)
# Si pas de numéro trouvé, retourner le titre tel quel
return folio_title
def update_xml_element(self, element, field_name, value):
"""Mettre à jour les informations de l'élément dans le XML"""
field_key = field_name.lower()
# Map inverse pour convertir les noms lisibles en noms XML
field_xml_map = {
'manufacturer ref': 'manufacturer_reference',
'internal ref': 'machine_manufacturer_reference',
'aux 1': 'auxiliary1',
'aux 2': 'auxiliary2'
}
# Utiliser le nom XML correct
xml_field_name = field_xml_map.get(field_key, field_key)
if field_key == 'label':
# Mettre à jour dans elementInformations
elem_infos = element.find('.//elementInformations')
if elem_infos is None:
elem_infos = ET.SubElement(element, 'elementInformations')
label_info = elem_infos.find("elementInformation[@name='label']")
if label_info is None:
label_info = ET.SubElement(elem_infos, 'elementInformation')
label_info.set('name', 'label')
label_info.set('show', '1')
label_info.text = value
# Aussi dans dynamic_texts
dynamic_texts = element.find('.//dynamic_texts')
if dynamic_texts is None:
dynamic_texts = ET.SubElement(element, 'dynamic_texts')
for text_elem in dynamic_texts.findall('.//dynamic_text'):
info_name = text_elem.find('info_name')
if info_name is not None and info_name.text == 'label':
text_content = text_elem.find('text')
if text_content is not None:
text_content.text = value
break
else:
# Mettre à jour dans elementInformations
elem_infos = element.find('.//elementInformations')
if elem_infos is None:
elem_infos = ET.SubElement(element, 'elementInformations')
info = elem_infos.find(f"elementInformation[@name='{xml_field_name}']")
if info is None:
info = ET.SubElement(elem_infos, 'elementInformation')
info.set('name', xml_field_name)
info.set('show', '1')
info.text = value
def export_csv(self):
"""Exporter les éléments en CSV"""
sheet_data = self.sheet.get_sheet_data()
if not sheet_data:
messagebox.showwarning("Attention", "Aucun élément à exporter")
return
file_path = filedialog.asksaveasfilename(
title="Exporter en CSV",
defaultextension=".csv",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
if not file_path:
return
try:
# Exclure la colonne UUID (index 0) pour l'export
columns = self.columns_order[1:]
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile, delimiter=';')
writer.writerow(columns)
for row in sheet_data:
# Ignorer la première colonne (UUID)
writer.writerow(row[1:])
messagebox.showinfo("Succès", f"Fichier CSV créé: {Path(file_path).name}")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de l'export:\n{str(e)}")
def add_element(self):
"""Ajouter un nouvel élément"""
if not self.tree_file:
messagebox.showwarning("Attention", "Veuillez d'abord ouvrir un fichier")
return
messagebox.showinfo("Non implémenté", "Ajout d'éléments non implémenté dans cette version")
def delete_element(self):
"""Supprimer l'élément sélectionné"""
selected_rows = self.sheet.get_selected_rows(return_tuple=True)
if not selected_rows:
messagebox.showwarning("Attention", "Sélectionnez des lignes")
return
if messagebox.askyesno("Confirmation", "Êtes-vous sûr de vouloir supprimer les éléments sélectionnés ?"):
# Récupérer les UUIDs avant suppression
sheet_data = self.sheet.get_sheet_data()
uuids_to_delete = [sheet_data[r][0] for r in selected_rows]
# Trouver et supprimer l'élément du XML
root = self.tree_file.getroot()
for diagram in root.findall('.//diagram'):
for element in diagram.findall('.//element'):
if element.get('uuid') in uuids_to_delete:
diagram.remove(element)
# Retirer de la liste
self.elements_data = [e for e in self.elements_data if e['uuid'] not in uuids_to_delete]
# Supprimer du tableau
self.sheet.delete_rows(list(selected_rows))
self.info_label.config(text=f"Éléments: {len(self.elements_data)}")
messagebox.showinfo("Succès", "Élément supprimé")
def save_file(self):
"""Sauvegarder les modifications"""
if not self.file_path or not self.tree_file:
messagebox.showwarning("Attention", "Aucun fichier ouvert")
return
try:
# Récupérer toutes les données du tableau
sheet_data = self.sheet.get_sheet_data()
# Créer un mapping UUID -> Element XML pour accès rapide
uuid_map = {}
root = self.tree_file.getroot()
for diagram in root.findall('.//diagram'):
for element in diagram.findall('.//element'):
uuid = element.get('uuid')
if uuid:
uuid_map[uuid] = element
# Mapping index colonne -> nom champ XML
# 0: UUID, 1: LABEL, 2: FABRICANT, etc.
col_map = {
1: 'label', 2: 'manufacturer', 3: 'designation', 4: 'description',
5: 'comment', 6: 'location', 7: 'plant', 8: 'auxiliary1',
9: 'auxiliary2', 10: 'function', 11: 'manufacturer_reference',
12: 'supplier', 13: 'machine_manufacturer_reference',
14: 'unity', 15: 'quantity'
}
for row in sheet_data:
uuid = row[0]
if uuid in uuid_map:
element = uuid_map[uuid]
for col_idx, field_name in col_map.items():
if col_idx < len(row):
val = str(row[col_idx]) if row[col_idx] is not None else ""
self.update_xml_element(element, field_name, val)
# Sauvegarder avec indentation
self.tree_file.write(self.file_path, encoding='utf-8', xml_declaration=True)
messagebox.showinfo("Succès", f"Fichier sauvegardé: {Path(self.file_path).name}")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de la sauvegarde:\n{str(e)}")
def toggle_columns(self):
"""Masquer/Afficher les colonnes optionnelles."""
if not self.elements_data:
messagebox.showwarning("Attention", "Aucun fichier chargé")
return
# Colonnes à masquer/afficher
columns_to_toggle = ['BLOC AUX 1', 'BLOC AUX 2', 'NUMERO DE COMMANDE', 'INSTALLATION',
'NUMERO INTERNE', 'UNITE', 'QUANTITE']
indices_to_toggle = {self.columns_order.index(c) for c in columns_to_toggle if c in self.columns_order}
# Obtenir la liste de toutes les colonnes visibles possibles (tout sauf UUID à l'index 0)
all_possible_indices = set(range(1, len(self.columns_order)))
if self.columns_hidden:
# L'état est "caché", donc on veut tout afficher.
self.sheet.displayed_columns = sorted(list(all_possible_indices))
self.columns_hidden = False
else:
# L'état est "affiché", donc on veut cacher les colonnes optionnelles.
new_displayed = all_possible_indices - indices_to_toggle
self.sheet.displayed_columns = sorted(list(new_displayed))
self.columns_hidden = True
self.sheet.redraw()
self.auto_resize_columns()
if __name__ == "__main__":
root = tk.Tk()
app = QETAnalyzer(root)
root.mainloop()
pip install pyinstaller
pyinstaller --noconsole --onefile QET_NOMENCLATURE.py
pour compiler en executable
very nice i was thinking about the same. thank u
I will translate it into german and share it with u
Please provide an little example project, pictures or video, if you can?
very nice i was thinking about the same. thank u
I will translate it into german and share it with u
English translation can help too.
I tried your script.
However, it didn't work to write information directly to the QET file.
Unfortunately, I find your version very confusing.
I have now taken my PLC manager and modified it functionally so that it has the same function.
This gives you a page-by-page breakdown of the components, and I have added the missing variables.
My German version is attached.
it's running for me to write in the qet files the propertie of each element.
Don't forget to save the project and open the saving project.
it's the first release for the moment and i used if to french diagram may be should be adapted to used with other language, in case of i think of properties don"t have the same name, i need to check later,
I will upgrade to use tksheet later also.
script updated tu use tksheet to copy paste from excel file
compile.bat
@echo off
:: Se placer dans le dossier du script actuel
cd /d "%~dp0"
echo ========================================================
echo COMPILATION DE QET_NOMENCLATURE EN EXECUTABLE (.EXE)
echo ========================================================
echo.
:: 1. Vérifier et installer les prérequis
echo [1/3] Verification et installation des librairies requises...
:: On s'assure que pyinstaller et tksheet sont bien installés
python -m pip install --upgrade pip
python -m pip install tksheet pyinstaller
if %errorlevel% neq 0 (
echo.
echo ERREUR: Impossible d'installer les librairies. Verifiez votre installation Python.
pause
exit /b
)
:: 2. Nettoyer les anciennes compilations pour éviter les conflits
echo.
echo [2/3] Nettoyage des anciens fichiers...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist *.spec del *.spec
:: 3. Lancer la compilation
echo.
echo [3/3] Creation de l'executable (cela peut prendre 1 ou 2 minutes)...
:: --noconsole : Masque la fenêtre noire (console) au lancement
:: --onefile : Crée un seul fichier .exe autonome (au lieu d'un dossier)
:: --hidden-import : Force l'inclusion de tksheet pour être sûr
pyinstaller --noconsole --onefile --hidden-import=tksheet --name "QET_Nomenclature" "QET_NOMENCLATURE.PY"
echo.
echo ========================================================
if exist "dist\QET_Nomenclature.exe" (
echo SUCCES !
echo Votre executable est pret dans le dossier : dist\QET_Nomenclature.exe
echo.
echo Vous pouvez copier ce fichier "QET_Nomenclature.exe" n'importe ou.
echo Il contient deja tksheet et Python.
) else (
echo ECHEC DE LA COMPILATION.
echo Veuillez verifier les messages d'erreur ci-dessus.
)
echo ========================================================
pause
QElectroTech → Scripts → script edition nomemclature
Powered by PunBB, supported by Informer Technologies, Inc.
Generated in 0.023 seconds (42% PHP - 58% DB) with 11 queries