import update_database import argparse import os import json import pandas as pd import openpyxl """ Dieses Programm kann: - eine Excel-Datei exportieren, in der alle Teilenummern aufgelistet sind, die in Verbindung mit einer angegebenen Nummer migriert werden müssen. Sie gibt dabei Auskunft darüber, welche Teilenummern bereits migriert wurden und welche noch migriert werden müssen. - die oben beschriebene Darstellung in einer Excel-Datei auch als Treeview auf der Kommandozeile darstellen """ class Compare: lib_dir = os.environ.get('CREMIG_LIB') data_dir = os.environ.get('CREMIG_DATA') out_dir = os.environ.get('CREMIG_WORK') ruledesigner_csv_filename = os.environ.get('RD_DATABASE_NAME') ruledesigner_csv_filepath = os.path.join(data_dir, ruledesigner_csv_filename) vba_macro_filename = os.environ.get('VBA_MACRO_NAME') vba_macro_filepath = os.path.join(lib_dir, vba_macro_filename) def __init__(self, main_sivas_id): self.main_sivas_id = main_sivas_id self.sivas_json_path = os.path.join(Compare.data_dir, main_sivas_id + ".json") self.sivas_ids = self.read_sivas_ids() self.ruledesigner_ids = self.read_ruledesigner_ids() def read_sivas_ids(self): # Metohde liest die JSON-Datei ein, in der alle Sivas-Teilenummern aufgelistet sind, die zusammen mit der Hauptnummer migriert werden müssen try: with open(self.sivas_json_path, encoding='utf-8') as json_file: return json.load(json_file) except FileNotFoundError: # JSON-Datei wurde noch nicht generiert update_database.get_sivas_dbase(self.main_sivas_id, Compare.data_dir) return self.read_sivas_ids() def read_ruledesigner_ids(self): # Methode liest die CSV-Datei ein, in der alle RuleDesigner-Nummern aufgelistet sind try: df = pd.read_csv(Compare.ruledesigner_csv_filepath, sep=';', converters={"Teilenummern":str}) return tuple(df["Teilenummern"]) except FileNotFoundError: # CSV-Datei wurde noch nicht ins %CREMIG_DATA% kopiert update_database.get_rd_dbase(Compare.data_dir) return self.read_ruledesigner_ids() def check_migration_status(self): # Methode prüft, ob die Sivas-Nummern bereits migriert wurden migrated = {} for id in self.sivas_ids: migrated[id] = True if id in self.ruledesigner_ids else False return migrated def create_sorting_string(self, id): # Methode setzt rekursiv einen String zusammen, der anzeigt, wo sich eine Teilenummer in der Hierarchie der obersten Baugruppe befindet. # Beispiel: # Die Teilenummer "123456" ist das Halbzeug der Teilenummer "234567". "234567" ist wiederum in der Baugruppe "345678" eingebaut. # Die Methode gibt dann folgenden String zurück: 345678-234567-123456 parent = self.sivas_ids[id][0]['Parent'] if parent not in self.sivas_ids: return f"{parent}-{id}" recur_val = self.create_sorting_string(parent) return f"{recur_val}-{id}" def create_excel_export(self): # Spalten in Excel-Datei: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung # Sortierung: Minus-separierter "Pfad" dieser Teilenummer im Assembly / im Halbzeug # Teilenummer: Sivas-Teilenummer # Bezeichnung: SIVAS-Bezeichnung der Teilenummer # Migrationsstatus: "IMPORTIEREN" = Teilenummer muss noch migriert werden, "VERFÜGBAR" = Teilenummer wurde bereits migriert # Restliche Spalten bleiben leer, weil sie in Excel befüllt werden sorting = [] prt_num = [] description = [] migration_status = [] evaluation = [] comment = [] migrated = self.check_migration_status() for id in self.sivas_ids: sorting.append(self.create_sorting_string(id)) prt_num.append(id) description.append(self.sivas_ids[id][0]['Bezeichnung']) migration_status.append("VERFÜGBAR" if migrated[id] is True else "IMPORTIEREN") evaluation.append('') comment.append('') data = { "Sortierung": sorting, "Teilenummer": prt_num, "Bezeichnung": description, "Migrationsstatus": migration_status, "Beurteilung": evaluation, "Bemerkung": comment } destiny_filepath = os.path.join(Compare.out_dir, self.main_sivas_id + ".xlsm") workbook = openpyxl.load_workbook(Compare.vba_macro_filepath, keep_vba=True) worksheet = workbook.active # Überschriften eintragen for header, column in zip(data, worksheet.iter_cols(min_row=1, max_col=len(data), max_row=1)): for cell in column: cell.value = header # Werte (unter Überschriften) eintragen for value, column in (zip(data.values(), worksheet.iter_cols(min_row=2, max_col=len(data), max_row=len(sorting)))): for cell, element in zip(column, value): cell.value = element # Umbenennung des Blattes ist erforderlich, weil VBA Makro erst wirksam wird, wenn das Blatt nicht mehr "Sheet1" heißt # Die Umbenennung darf nicht vor der Eintragung der Daten erfolgen, weil es sonst Probleme mit der bedingten Formatierung gibt worksheet.title = "Ergebnis" workbook.save(destiny_filepath) print(f"Excel-Datei erfolgreich in '{Compare.out_dir}' geschrieben...") def parent_children_mapping(self): # Methode erstellt dict mit allen Parents (Keys) der json-Datei und weist den Parents deren Kinder als Werte zu. parent_children = {} for id in self.sivas_ids: parent = self.sivas_ids[id][0]["Parent"] if parent not in parent_children: parent_children[parent] = [] parent_children[parent].append(id) return parent_children def get_excel_comments_for_treeview(self): workbook = openpyxl.load() pass def create_treeview(self): symbols = { "pipe": "│ ", "empty": " ", "elbow": "└──", "t": "├──" } # ANSI-Escape-Sequenzen für Einfärbung des Terminals abhängig vom Migrationsstatus. # os.system wird für die Verarbeitung der Sequenzen benötigt os.system("color") colors = { "RED": "\033[41m", "GREEN": "\033[42m", "RESET": "\033[0m" } prefix = "" parent_children = self.parent_children_mapping() max_tree_depth = max(id_value[0]["Position"].count(".") for id_value in self.sivas_ids.values()) max_sivas_num_len = max([len(id) for id in self.sivas_ids.keys()]) max_description_len = max([len(id_value[0]["Bezeichnung"]) for id_value in self.sivas_ids.values()]) for i, id in enumerate(self.sivas_ids): parent = self.sivas_ids[id][0]["Parent"] description = self.sivas_ids[id][0]["Bezeichnung"] current_key_level = self.sivas_ids[id][0]["Position"].count(".") # Aktueller Verbinder: └── , wenn letzter Knotenpunkt, sonst ├── connector = symbols['elbow'] if id == parent_children[parent][-1] else symbols['t'] # grün = Teilenummer migriert, rot = Teilenummer noch nicht migriert out_color = f"{colors['GREEN']}" if id in self.ruledesigner_ids else f"{colors['RED']}" if i == 0: # Bei oberster Teilenummer soll kein Verbinder dargestellt werden. print(f"{out_color}{parent}{colors['RESET']}") tree_str = f"{prefix}{connector}{out_color}{id}{colors['RESET']}" len_tree_str = len(tree_str) - len(out_color + colors['RESET']) pad_tree = (max_tree_depth * len(symbols['pipe']) + len(symbols['t']) + max_sivas_num_len - len_tree_str + 2) * " " pad_description = (max_description_len - len(description) + 2) * " " print(f"{tree_str}{pad_tree}{description}{pad_description}") if (i + 1) != len(self.sivas_ids): next_key = list(self.sivas_ids)[i + 1] next_key_level = self.sivas_ids[next_key][0]["Position"].count(".") else: # Zu migrierende Nummer ist ein bearbeitetes Halbzeug. Programm muss hier beendet werden, da ansonsten später die # nächste Teilenummer der json benötigt wird. Diese exisitert aber nicht, weil die json nur eine Teilenummer enthält break if id == parent_children[parent][-1]: # Prüft, ob die aktuelle Nummer das letzte Kind des Parents ist if id in parent_children: # Prüft, ob das aktuelle Teil selbst ein Parent ist. # Wenn ja, muss unter dem connector-Symbol der aktuellen Nummer (also im nächsten Durchlauf) Leerraum stehen prefix += symbols["empty"] else: # Ende der aktuellen Gruppe an Teilenummern, die sich auf einer gemeinsamen Ebene befinden ist erreicht. # Die nächste Teilenummer liegt eine oder mehrere Ebenen höher als die aktuelle Teilenummer (außer das Ende des gesamten Baumes ist erreicht). # Der bisherige Präfix-String wird deshalb gekürzt. Die Anzahl der zu kürzenden Zeichen ergibt sich durch die Anzahl der Ebenen, die zwischen # der aktuellen Teilenummer und der darauffolgenden Teilenummer liegen und der Länge des Pipe-Symbols, bzw. eines anderen Symbols (alle Symbole # haben die gleiche Länge) prefix_trim_len = (next_key_level - current_key_level) * len(symbols["pipe"]) prefix = prefix[:prefix_trim_len] else: if id in parent_children: # Prüft erneut, ob das aktuelle Teil auch ein Parent ist. # Wenn ja, muss dieses Mal unter der nächsten Nummer ein Pipe-Symbol kommen prefix += symbols["pipe"] if __name__ == '__main__': parser = argparse.ArgumentParser(description='Das Programm gibt Auskunft über den aktuellen Migrationsstand', prog='compare_lists') parser.add_argument('-n', '--number', action='store', required=True, help='Eingabe der SIVAS-Teilenummer, für die der Migrationsstatus angezeigt werden soll', metavar="id") parser.add_argument('-t', '--tree', action='store_true', help='Erzeugt Baumdarstellung aller Teilenummern. grün = Teilenummer vorhanden, rot = Migration ausstehend') parser.add_argument('-e', '--excel', action='store_true', help='Erzeugt Excel-Datei in der alle Teilenummern, die migriert werden müssen, aufgelistet sind') args = parser.parse_args() if args.excel: num = args.number.lstrip() excel_comparison = Compare(num) excel_comparison.create_excel_export() elif args.tree: num = args.number.lstrip() treeview_comparison = Compare(num) treeview_comparison.create_treeview() else: parser.print_help()