1 (edited by javdenech 2026-02-07 14:09:06)

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...

2 (edited by javdenech 2026-02-16 20:40:35)

Re: script edition nomemclature

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()

Re: script edition nomemclature

pip install pyinstaller
pyinstaller --noconsole --onefile QET_NOMENCLATURE.py
pour compiler en executable

4 (edited by Kellermorph 2026-02-07 16:40:28)

Re: script edition nomemclature

very nice i was thinking about the same. thank u
I will translate it into german and share it with u

Re: script edition nomemclature

Please provide an little example project, pictures or video, if you can?

"Le jour où tu découvres le Libre, tu sais que tu ne pourras jamais plus revenir en arrière..."Questions regarding QET belong in this forum and will NOT be answered via PM! – Les questions concernant QET doivent être posées sur ce forum et ne seront pas traitées par MP !

Re: script edition nomemclature

Kellermorph wrote:

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.

"Le jour où tu découvres le Libre, tu sais que tu ne pourras jamais plus revenir en arrière..."Questions regarding QET belong in this forum and will NOT be answered via PM! – Les questions concernant QET doivent être posées sur ce forum et ne seront pas traitées par MP !

Re: script edition nomemclature

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.

Post's attachments

Attachment icon Eigenschaften.py 16.78 kb, 28 downloads since 2026-02-09 

8 (edited by javdenech 2026-02-10 22:38:50)

Re: script edition nomemclature

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.

Re: script edition nomemclature

script updated tu use tksheet to copy paste from excel file

Re: script edition nomemclature

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