Hello scorpio; je fais une video dès que possible

27

(9 replies, posted in Scripts)

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

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

29

(9 replies, posted in Scripts)

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

import xml.etree.ElementTree as ET
import tkinter as tk
from tkinter import filedialog, messagebox
import os

class QETRenumberingApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Numérotation de Fils QElectroTech")
        self.root.geometry("700x550")

        # Variables de données
        self.file_path = None
        self.tree = None
        self.root_xml = None
        self.folios = [] # Liste des éléments <diagram>

        # --- Interface Graphique ---
        
        # Section Fichier
        frame_file = tk.LabelFrame(root, text="Fichier Projet")
        frame_file.pack(fill=tk.X, padx=10, pady=5)
        
        self.btn_open = tk.Button(frame_file, text="Ouvrir fichier .qet", command=self.open_file)
        self.btn_open.pack(side=tk.LEFT, padx=5, pady=5)
        
        self.lbl_file = tk.Label(frame_file, text="Aucun fichier sélectionné", fg="gray")
        self.lbl_file.pack(side=tk.LEFT, padx=5)

        # Section Liste des Folios
        frame_list = tk.LabelFrame(root, text="Sélection du Folio")
        frame_list.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        self.listbox_folios = tk.Listbox(frame_list, selectmode=tk.SINGLE, font=("Consolas", 10))
        self.listbox_folios.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        scrollbar = tk.Scrollbar(frame_list, orient="vertical", command=self.listbox_folios.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox_folios.config(yscrollcommand=scrollbar.set)
        self.listbox_folios.bind('<<ListboxSelect>>', self.on_folio_select)

        # Section Options
        frame_options = tk.LabelFrame(root, text="Paramètres de Numérotation")
        frame_options.pack(fill=tk.X, padx=10, pady=5)

        tk.Label(frame_options, text="Préfixe :").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        self.entry_prefix = tk.Entry(frame_options, width=10)
        self.entry_prefix.grid(row=0, column=1, padx=5, pady=5, sticky="w")
        self.entry_prefix.insert(0, "") # Vide par défaut

        tk.Label(frame_options, text="Format :").grid(row=0, column=2, padx=15, pady=5, sticky="e")
        self.var_format = tk.StringVar(value="0")
        tk.Radiobutton(frame_options, text="0, 1, 2...", variable=self.var_format, value="0").grid(row=0, column=3)
        tk.Radiobutton(frame_options, text="00, 01, 02...", variable=self.var_format, value="00").grid(row=0, column=4)

        tk.Label(frame_options, text="Début :").grid(row=1, column=0, padx=5, pady=5, sticky="e")
        self.entry_start = tk.Entry(frame_options, width=10)
        self.entry_start.grid(row=1, column=1, padx=5, pady=5, sticky="w")
        self.entry_start.insert(0, "0")

        self.btn_set_08 = tk.Button(frame_options, text="Mettre 08", command=lambda: self.set_start_value("08"))
        self.btn_set_08.grid(row=1, column=2, padx=5, pady=5, sticky="w")

        # Section Actions
        frame_actions = tk.Frame(root)
        frame_actions.pack(fill=tk.X, padx=10, pady=10)

        self.btn_process = tk.Button(frame_actions, text="Numéroter le folio sélectionné", command=self.process_folio, state=tk.DISABLED, bg="#dddddd", height=2)
        self.btn_process.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

        self.btn_save = tk.Button(frame_actions, text="Sauvegarder le projet sous...", command=self.save_file, state=tk.DISABLED, height=2)
        self.btn_save.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=5)

    def set_start_value(self, val):
        self.entry_start.delete(0, tk.END)
        self.entry_start.insert(0, val)

    def open_file(self):
        path = filedialog.askopenfilename(filetypes=[("Projet QElectroTech", "*.qet"), ("Fichiers XML", "*.xml")])
        if path:
            self.file_path = path
            self.lbl_file.config(text=os.path.basename(path), fg="black")
            self.parse_qet()

    def parse_qet(self):
        try:
            self.tree = ET.parse(self.file_path)
            self.root_xml = self.tree.getroot()
            self.folios = []
            self.listbox_folios.delete(0, tk.END)

            # Recherche des diagrammes (folios)
            for i, diagram in enumerate(self.root_xml.findall('diagram')):
                title = diagram.get('title', 'Sans titre')
                # Essayer de trouver le numéro de folio, sinon utiliser l'index
                num = diagram.get('folio', str(i + 1))
                label = f"N° {num} : {title}"
                self.folios.append(diagram)
                self.listbox_folios.insert(tk.END, label)
            
            self.btn_save.config(state=tk.NORMAL)
            self.btn_process.config(state=tk.DISABLED)

        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible de lire le fichier :\n{e}")

    def on_folio_select(self, event):
        if self.listbox_folios.curselection():
            self.btn_process.config(state=tk.NORMAL, bg="#aaffaa") # Vert clair quand actif
            
            # Réinitialiser le début selon le format
            default_val = "00" if self.var_format.get() == "00" else "0"
            self.entry_start.delete(0, tk.END)
            self.entry_start.insert(0, default_val)
        else:
            self.btn_process.config(state=tk.DISABLED, bg="#dddddd")

    def process_folio(self):
        idx = self.listbox_folios.curselection()
        if not idx:
            return
        
        diagram = self.folios[idx[0]]
        prefix = self.entry_prefix.get()
        fmt = self.var_format.get()
        
        try:
            start_num = int(self.entry_start.get())
        except ValueError:
            start_num = 0

        try:
            count = self.renumber_wires_logic(diagram, prefix, fmt, start_num)
            messagebox.showinfo("Succès", f"Folio renuméroté avec succès.\n{count} équipotentielles traitées.")
        except Exception as e:
            messagebox.showerror("Erreur", f"Une erreur est survenue lors de la numérotation :\n{e}")

    def get_definitions(self, root_xml):
        definitions = {}
        collection = root_xml.find('collection')
        if collection is None:
            return definitions

        def traverse(element, current_path):
            for child in element:
                if child.tag == 'category':
                    name = child.get('name')
                    new_path = f"{current_path}/{name}" if current_path else name
                    traverse(child, new_path)
                elif child.tag == 'element':
                    name = child.get('name')
                    uri = f"embed://{current_path}/{name}" if current_path else f"embed://{name}"
                    definition = child.find('definition')
                    if definition is not None:
                        definitions[uri] = definition

        traverse(collection, "")
        return definitions

    def renumber_wires_logic(self, diagram, prefix, fmt, start_num=0):
        # 0. Index definitions
        definitions = self.get_definitions(self.root_xml)

        # 1. Identification des bornes de continuité et positions des terminaux
        continuity_uuids = set()
        terminals_pos = {} # (element_uuid, terminal_uuid) -> (x, y)
        elements_pos = {} # uuid -> (x, y) pour position par défaut

        for elem in diagram.findall('elements/element'):
            uuid = elem.get('uuid')
            type_uri = elem.get('type', '')
            
            # Stockage position pour tri géométrique si le fil n'a pas de points
            try:
                x = float(elem.get('x', 0))
                y = float(elem.get('y', 0))
                elements_pos[uuid] = (x, y)
            except:
                continue

            # Détection borne continuité
            if 'continuite' in type_uri.lower():
                continuity_uuids.add(uuid)
            
            # Parse terminals from definition
            def_node = definitions.get(type_uri)
            if def_node is not None:
                # Check link_type in definition if needed
                if 'continuite' in def_node.get('link_type', '').lower():
                     continuity_uuids.add(uuid)

                for term in def_node.findall('description/terminal'):
                    t_uuid = term.get('uuid')
                    if t_uuid:
                        try:
                            t_x = float(term.get('x', 0))
                            t_y = float(term.get('y', 0))
                            terminals_pos[(uuid, t_uuid)] = (x + t_x, y + t_y)
                        except:
                            pass

        # 2. Construction du graphe de connexions (Union-Find)
        conductors = diagram.findall('conductors/conductor')
        parent = list(range(len(conductors)))

        def find(i):
            if parent[i] == i: return i
            parent[i] = find(parent[i])
            return parent[i]

        def union(i, j):
            root_i = find(i)
            root_j = find(j)
            if root_i != root_j:
                parent[root_i] = root_j

        # Dictionnaire : (ElementUUID, TerminalUUID) -> Liste d'index de conducteurs
        terminals_map = {}

        for i, cond in enumerate(conductors):
            # Récupération des extrémités du fil
            e1_uid = cond.get('element1')
            t1_uid = cond.get('terminal1')
            e2_uid = cond.get('element2')
            t2_uid = cond.get('terminal2')

            # Enregistrement des connexions
            for e_uid, t_uid in [(e1_uid, t1_uid), (e2_uid, t2_uid)]:
                if not e_uid: continue

                # Si c'est une borne de continuité, on ignore le terminal spécifique
                # pour considérer que tout ce qui touche cet élément est connecté.
                if e_uid in continuity_uuids:
                    key = (e_uid, 'COMMON_POTENTIAL')
                else:
                    key = (e_uid, t_uid)

                if key not in terminals_map:
                    terminals_map[key] = []
                terminals_map[key].append(i)

        # Fusion des groupes (Union)
        for key, indices in terminals_map.items():
            base = indices[0]
            for other in indices[1:]:
                union(base, other)

        # 3. Regroupement par équipotentielle
        groups = {}
        for i, cond in enumerate(conductors):
            root = find(i)
            if root not in groups:
                groups[root] = []
            groups[root].append(cond)

        # 4. Filtrage et Tri
        equipotentials_to_process = []

        for root, group in groups.items():
            # Vérifier si l'équipotentielle est déjà numérotée (verrouillée)
            # On ignore si un des fils a un numéro qui n'est pas "" ou "_"
            is_locked = False
            for cond in group:
                num = cond.get('num', '')
                if num and num != "_":
                    is_locked = True
                    break
            
            if is_locked:
                continue

            # Calcul de la position (le point le plus en haut à gauche de tout le réseau)
            min_x = float('inf')
            min_y = float('inf')

            for cond in group:
                # Vérifier les points du tracé du fil
                points = cond.findall('point')
                if points:
                    for pt in points:
                        px = float(pt.get('x', 0))
                        py = float(pt.get('y', 0))
                        if px < min_x: min_x = px
                        if py < min_y: min_y = py
                else:
                    # Si pas de points (connexion directe), utiliser la position des terminaux
                    e1 = cond.get('element1')
                    t1 = cond.get('terminal1')
                    if e1 and t1 and (e1, t1) in terminals_pos:
                        tx, ty = terminals_pos[(e1, t1)]
                        if tx < min_x: min_x = tx
                        if ty < min_y: min_y = ty
                    elif e1 and e1 in elements_pos:
                        ex, ey = elements_pos[e1]
                        if ex < min_x: min_x = ex
                        if ey < min_y: min_y = ey
                    
                    e2 = cond.get('element2')
                    t2 = cond.get('terminal2')
                    if e2 and t2 and (e2, t2) in terminals_pos:
                        tx, ty = terminals_pos[(e2, t2)]
                        if tx < min_x: min_x = tx
                        if ty < min_y: min_y = ty
                    elif e2 and e2 in elements_pos:
                        ex, ey = elements_pos[e2]
                        if ex < min_x: min_x = ex
                        if ey < min_y: min_y = ey
            
            # Sécurité si infini
            if min_x == float('inf'): min_x = 0
            if min_y == float('inf'): min_y = 0

            equipotentials_to_process.append({
                'wires': group,
                'x': min_x,
                'y': min_y
            })

        # Tri : Haut en Bas (y), puis Gauche à Droite (x) pour un balayage par ligne.
        equipotentials_to_process.sort(key=lambda e: (e['y'], e['x']))

        # 5. Application de la numérotation
        counter = start_num
        for eq in equipotentials_to_process:
            # Formatage du numéro
            num_part = str(counter)
            if fmt == "00" and counter < 10:
                num_part = "0" + num_part
            
            label = f"{prefix}{num_part}"

            # Mise à jour de tous les fils du groupe
            for cond in eq['wires']:
                cond.set('num', label)
                # Note: QET met à jour l'affichage automatiquement basé sur l'attribut 'num'
            
            counter += 1
        
        return len(equipotentials_to_process)

    def save_file(self):
        if not self.tree: return
        
        path = filedialog.asksaveasfilename(defaultextension=".qet", filetypes=[("Projet QElectroTech", "*.qet")])
        if path:
            try:
                self.tree.write(path, encoding="UTF-8", xml_declaration=True)
                messagebox.showinfo("Sauvegardé", f"Fichier sauvegardé :\n{path}")
            except Exception as e:
                messagebox.showerror("Erreur", f"Erreur lors de la sauvegarde :\n{e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = QETRenumberingApp(root)
    root.mainloop()

Voici un script que j'ai créer pour numéroter les fils par folio avec préfixe qui exclu les fils déjà numérotés, pratique pour les io.
la numérotation s'effectue de bas en haut gauche droite en sélectionnant folio par folio.

ne prend en charge qui si les borne ont un nom en francais "continuité"

32

(9 replies, posted in Scripts)

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

Bonjour tous
On pourrais ajouter un bouton rapide pour remplacer en un clic la formule d'un conducteur resultant de l'autonumerotation par son résultat. ( j'ai de temps en temps des numéros de fil qui tiennent compte des folios alors c'est un peu embétant avec les renvois)

34

(554 replies, posted in News)

oui le type d'autonumerotation m+chifre est selectionné ainsi que celui des fils lors du multipaste.

(bug identique sous windows 7)

35

(554 replies, posted in News)

sur mac je pose un contacteurs puis un moteur 
les trois phase se numerote sauf la troisieme
tu pose le moteur puis le contacteurs, les trois phases se numerote correctement


multipaste sur les moteurs, M1 M1 M1 M1 M1 avec autonumerotation des elements cocher
multipaste sur les contacteur avec autoconnexion, les fils s'incrementent correctement

il y a un bug sur l'autonumerotation des fils dans les deux cas entre l'objet inferieur et l'objet superieur
qui se replique lors du multipaste....

36

(554 replies, posted in News)

@scorpio 
oui a la maison, mais j'ai des sites web en cours plus le temps
donc du coup je suis sur windows 7 au bureau.

Pour l'instant pas de bug sur les textes dynamiques, cela fonctionne carrément bien.
je ferais un test sur le mac des que j'ai 5 minutes

37

(554 replies, posted in News)

Peut être un rappel d'une configuration de texte sur plusieurs symboles simultanément via un clic droit
pour les schémas existant
nomicons/grin
ça serait super cool comme fonction

38

(554 replies, posted in News)

@JOSHUA
Effectivement, ça du être l'usine à gaz...

39

(554 replies, posted in News)

@joshua
peut être que l'on pourrait la garder cette case a coché et faire en sorte que le commentaire s’intègre systématiquement dans la liste de texte dynamique si celui ci n'y est pas déja
ça doit faire un peu de taf a codé........mais cela permettrait de récupérer les projets qui ont été fait avec label + commentaire.
je pense que je dois pas être le seul dans ce cas la
j'ai essayé en éditant le projet en xml mais c'est galère....

40

(554 replies, posted in News)

Salut tous

j'ai repris la dernière version et j'avais des commentaires qui étaient coché en affichage sur mes symboles dans le folio.
mais malheureusement lors de la conversion en texte dynamique, je suis obligé de les rajouter dans les texte dynamique à la mano sur chaque symbole.
Aurais je loupé un épisode ou si il y a une manip simple pour rajouté le champ commentaire sur une série de symboles déja dessiné ????
Yé suis preneur

41

(554 replies, posted in News)

d

42

(554 replies, posted in News)

b

43

(554 replies, posted in News)

a

44

(554 replies, posted in News)

Nuri wrote:

...euh... J'ai commencer à faire la liste de toutes les propriétés en m'inspirant du logiciel commercial allemand qu'on m'oblige à utiliser, puis je me suis dit : "mais attends, avant de passer 1 heure là-dessus, est-ce vraiment nécessaire ?"

Pour vous donner une idée, ces propriétés sont, en vrac :
- données fabricant (nom du fabricant, référence, description, etc.)
- données fournisseur (nom du fournisseur, numéro de cammande, etc.)
- données géométriques (hauteur, largeur, profondeur, diamètre)
- données commerciales (prix, monnaie, rabait, etc.)
- données pour la maintenance (pièce de rechange, article en fin de vie, etc.)
- ...
C'est un sacrée catalogue ! J'ai pas compté, mais c'est une bonne centaine de propriétés pour 1 seul article nomicons/blink
Certes, ces infos peuvent être intéressantes, mais personne n'a le temps de renseigner tous ces machins. J'ai cottoyé assez de bureaux d'étude pour savoir que la plupart des champs restent vides...

A tout bien y réfléchir, je pense que les données fournisseur (nom et numéro de commande) n'ont pas leur place dans QET car ce sont des données personnelles. Si on veut facilement partager nos collections, il faut exclure les données personnelles.

De toute facon, galexis et javdenech gèrent leurs achats avec des fichiers externes, si j'ai bien compris. C'est sûrement la bonne méthode. Comme ca les prix et autres données commerciales (délais de livraison, etc.) peuvent être actualisées par une personne autre que le concepteur des plans électriques (ce qui est très souvent le cas).

Perso, moi je suis satisfait avec les champs d'infos déjà disponibles dans QET. Si on en rajoute, on risque de transformer QET en usine à gaz...

@ galexis et javdenech :
pour votre "numéro de commande" fournisseur, vous devriez plutôt utliser le champ "numéro interne" car c'est le numéro que vous utilisez "en interne" pour faire le lien avec vos fichiers externe (xls, ods ou autre).
Enfin... vous bossez comme vous voulez, bien sûr, je voudrais juste qu'on se mette d'accord sur la structure des données pour faciliter plus tard les échanges.

nuri
tu as bien raison finalement ca sert a rien
Suffit que phoenix te change le short item code et c'est mort.
je ne les utilise jamais, ingérable

45

(554 replies, posted in News)

Joshua wrote:
Nuri wrote:

Et quand on spécifie du matos, on précise, au minimum, le nom du FABRICANT, la référence, le numéro de commande et une petite description textuelle histoire de capter rapidement de quoi il s'agit.
Quand j'ai fini mes plans, j'envoie la nomenclature à mon client qui se débrouille tout seul avec ses FOURNISSEURS.
En fait, je connais quasiment jamais le nom des fournisseurs de mes clients, ca sort déjà du cadre de mon travail.

Tu dit au minimum : Fabricant, référence, numéro de commande et petite description.
Ensuite tu dit "le client se débrouille avec sont fournisseur".
Mais le numéro de commande, c'est bien un numéro propre au fournisseur. Du coup tu devrais juste avoir à renseigner Fabricant, référence, et petite description.
Je me trompe ?
J'avoue n'avoir pas trop réfléchie au truc, je suis comme Laurent, quand je refais un schéma éléc, je renseigne uniquement le nom du fabricant, la référence "fabricant" du produit et un petit texte d’explication.

C'est le comble, je suis le développeur, mais j'utilise pas souvent QET dans mon boulot, et encore moins de manière aussi poussé que vous nomicons/laughing

C'est pas faux joshua
Je le gère avec un fichier excel qui me retrouve les références fournisseur pour balancer automatiquement les commandes dans notre logiciel pmi ( qui au passage sait déjà gérer des codes articles interne ) mais on ne s'en sert pas, trop lourdingue. (c'est aussi ce que fait une gmao sur un site de production)
Pour l'évolution des prix, j'ai une fonction de mise à jour à chaque réception de devis avec un historique.
C'est avec ce même fichier excel que je balance mes nomenclatures en fin de schéma ( avec la réfèrence constructeur) le fournisseur n'étant pas forcément le constructeur...

46

(554 replies, posted in News)

Joshua wrote:

Si au lieu d'avoir :
"Numéro d'article", on avait "Numéro d'article fabricant"
et
"Numéro de commande", on avait "Numéro de commande fabricant"
Est-ce que cela supprimerait les confusions et contenterait tout le monde ?

PS
Les groupes de textes avancent nomicons/smile

numéro d'article fabricant (omron)
numéro de commande fournisseur (Rexel)

Alimentation découpage mono 240W 10 24VCC (374870)
Réf Rexel : OMRS8VK-G24024
Réf Fab : S8VK-G24024
EAN13 : 4548583357730

47

(554 replies, posted in News)

scorpio810 wrote:

Mais je pensais plutôt a faire en sorte que l'attribution des variables se fassent directement dans l’éditeur d’élément
en plaçant un texte composé, pouvoir ajouté les variables a ce moment la 

Heu Xavier nomicons/smiley-green , autant mettre un texte user dans l’éditeur et coller dedans les variables, le but c'est justement de pouvoir retravailler les textes d'un élément en live dans l’éditeur de schémas, et adapter l’élément au projet ou affaire.

nomicons/getlost En fait quand, je fais mes schémas, je crée ma nomenclature et ma liste d'entree sortie automate avant pour pouvoir commandé le materiel.
 Je me retrouve avec un fichier excel ou j'ai déjà tous mes labels references et rôles de chaque element ainsi que la marque sur mon écran de droite. ( Via VBA, je genere des image de 25 lignes de nomenclature que je colle directement dans q elec, plus malin plus rapide)
quand je pose mon symbole je clique dessus pour afficher la fenêtre propriété information puis je copie colle mon label. 
Dans le champ commentaire j'y met le role et la reference, ( en fait pour un var mx2 omron le symbole est le meme pour différente reference...; idem pour les var 1S etc et pour la plupart des NX IO.
Donc du coup je trouvais ça plus rapide de copier directement dans le champ description textuelle et reference en positon x y que d'aller dans l'onglet texte.
Recréer un symbole a chaque fois que j'ai une reference qui change pour un symbole identique, c'est un peut lourdingue... ( le reference je l'affiche pour les cableurs plus simple pour eux, moins de boulette)

48

(554 replies, posted in News)

scorpio810 wrote:

Petite video QElectroTech V 0.70-dev+svn5093

ok merci laurent 

Mais je pensais plutôt a faire en sorte que l'attribution des variables se fassent directement dans l’éditeur d’élément
en plaçant un texte composé, pouvoir ajouté les variables a ce moment la 

Le symbole dans la collection utilisateur aurait la config mémorisé, pas besoin de refaire la manip et de copier coller les symboles

49

(554 replies, posted in News)

Un texte = label ou fonction ou tension protocole, ou une des valeur : description textuel, reference fabricant avec une case a coché pour l'encadrer et une variable numerique de. numero de groupe pour encadre plusieurs textes dynamique ensemble, si les valeurs sont null, pas de underscore et pas de cadre

50

(554 replies, posted in News)

si tu pouvais les definir directement a la source dans l'editeur d'element,  ca serait top et plus simple a l'utilisation