from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus, TeilestammMetadata import argparse import os 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 Darstellung in der Excel-Datei als Baum in der Konsole ausgeben """ class ExcelExport: def __init__(self, asm_id, asm_json_dir, asm_json_filename, asm_csv_dir, asm_csv_filename, rd_dbase_dir, rd_dbase_filename, load_local_asm_json, load_local_asm_csv, load_local_rd_dbase, no_blue_ids): self.asm_id = asm_id self.asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename, load_local_asm_json) self.asm_metadata = TeilestammMetadata(asm_csv_dir, asm_csv_filename, load_local_asm_csv) self.rd_data = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename, load_local_rd_dbase) self._migration_status = None @property def migration_status(self): if self._migration_status is None: self._migration_status = MigrationStatus(self.asm_data.child_ids, self.rd_data.ids) return self._migration_status def write_excel(self): data = self._create_excel_data() out_dir = os.environ.get("CREMIG_WORK") out_path = os.path.join(out_dir, self.asm_id + ".xlsm") workbook_path = os.path.join(os.environ.get("CREMIG_LIB"), os.environ.get("VBA_MACRO_NAME")) workbook = openpyxl.load_workbook(workbook_path, keep_vba=True) worksheet = workbook.active # Spaltenüberschriften 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 Spaltenüberschriften for value, column in zip(data.values(), worksheet.iter_cols(min_row=2, max_col=len(data), max_row=len(data["Sortierung"])+1)): for cell, element in zip(column, value): cell.value = element # Umbenennung des Blattes erforderlich, da die Umbenennung die Ausführung des VBA-Makros triggert. # Die Umbenennung muss NACH dem Einfügen der Daten in das Blatt erfolgen, da die bedingte Formatierung, # die ebenfalls Bestandteil des Makros ist, nicht ausgeführt werden kann, solange sich keine Daten im Blatt befinden worksheet.title = "Ergebnis" workbook.save(out_path) print(f"Excel-Datei erfolgreich in '{out_dir}' geschrieben...") def _create_excel_data(self) -> dict: # Spalten im Arbeitsblatt: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung # Sortierung: Minus-separierte Hierarchie der Teilenummern in der Stücklistenstruktur # Teilenummer: SIVAS-Teilenummer # Bezeichnung: SIVAS-Bezeichnung der Teilenummer # Migrationsstatus: "IMPORTIEREN" = Migration ausstehend, "VERFÜGBAR" = Migration abgeschlossen # Alle weiteren Spalten bleiben leer, da sie händisch vom User zu befüllen sind sorting = self._create_sorting_string() migrated_ids = self.migration_status.ids_in_rd child_descriptions:dict = self.asm_data.get_bom_property_values("Bezeichnung") parent_description:str = self.asm_metadata.get_property_values("Bezeichnung1") descriptions = child_descriptions.copy() descriptions[asm_id] = parent_description prt_num = [] description = [] migration_status = [] assessment = [] comment = [] for id_str in sorting: id = id_str.split("-")[-1] prt_num.append(id) assessment.append('') comment.append('') if id not in descriptions: description.append("") else: description.append(descriptions[id]) if id in migrated_ids: if id in self.migration_status.ids_in_progress: migration_status.append("PDM-Migration abgeschlossen") else: migration_status.append("Migration abgeschlossen (freigegeben)") else: migration_status.append("Migration ausstehend") data = { "Sortierung": sorting, "Teilenummer": prt_num, "Bezeichnung": description, "Migrationsstatus": migration_status, "Beurteilung": assessment, "Bemerkung": comment } return data def _create_sorting_string(self, parent=None, previous_parents="", sorting_strings=None) -> list: """ Creates a string which represents the hierarchy of a partnumber in the SIVAS-BOM (bill of material). Example: The string "1000-1100-1110-1111" for the partnumber "1111" would represent the following BOM structure: 1000 └──1100 └──1110 └──1111 """ if parent is None: parent = self.asm_id if sorting_strings is None: sorting_strings = [parent] parent_children = self.asm_data.parent_children children = parent_children[parent] for child in children: sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-")) if child in parent_children: self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings) return sorting_strings class Treeview: # For correct display of ANSI escape sequences os.system("color") prefix_types = {"PIPE": "│ ", "EMPTY": " " } connector_symbols = {"ELBOW": "└──", "T": "├──" } console_colors = {"RED": "\033[41m", "GREEN": "\033[42m", "BLUE": "\033[44m", "RESET": "\033[0m" } def __init__(self, asm_id, asm_json_dir, asm_json_filename, asm_csv_dir, asm_csv_filename, rd_dbase_dir, rd_dbase_filename, load_local_asm_json, load_local_asm_csv, load_local_rd_dbase, no_blue_ids): self.asm_id = asm_id self.asm_bom_data = SivasAssemblyData(asm_json_dir, asm_json_filename, load_local_asm_json) self.asm_metadata = TeilestammMetadata(asm_csv_dir, asm_csv_filename, load_local_asm_csv) self.rd_data = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename, load_local_rd_dbase) self.no_blue_ids = no_blue_ids self._migration_status = None self._longest_entry = None @property def migration_status(self): if self._migration_status is None: self._migration_status = MigrationStatus(self.asm_bom_data.child_ids, self.rd_data.ids) return self._migration_status @property def longest_entry(self): if self._longest_entry is None: longest_color_code = max(Treeview.console_colors.values(), key=len) longest_connector_symbol = max(Treeview.connector_symbols.values(), key=len) padding_after_id = 2 if self._longest_entry is None: self._longest_entry = (self.asm_bom_data.max_bom_depth) * len(Treeview.prefix_types["PIPE"]) \ + len(longest_connector_symbol) \ + len(longest_color_code) \ + len(self.asm_bom_data.longest_child_id) \ + len(Treeview.console_colors["RESET"]) \ + padding_after_id return self._longest_entry def generate_tree(self): self._tree_header() self._tree_body() self._tree_summary() def _tree_header(self): """Prints root number (number for which the user wants to see the tree) to the console""" id = self.asm_id color = self._set_text_background_color(id) text_before_description = f"{color}{id}{Treeview.console_colors['RESET']}".ljust(self.longest_entry) description = self.asm_metadata.get_property_values("Bezeichnung1") print(f"{text_before_description}{description}") def _tree_body(self, parent_id=None, prefix=""): """Prints the body (all partnumbers except root/main-partnumber) of the tree to the console""" if parent_id is None: parent_id = self.asm_id parent_children = self.asm_bom_data.parent_children children_ids = parent_children[parent_id] for index, child_id in enumerate(children_ids): is_last_assembly_child = self._last_child_check(index, children_ids) connector_symbol = self._set_connector_symbol(is_last_assembly_child) partnumber_background_color = self._set_text_background_color(child_id) self._print_entry(child_id, prefix, connector_symbol, partnumber_background_color) if child_id in parent_children: next_prefix = self._prefix(prefix, is_last_assembly_child) self._tree_body(parent_id=child_id, prefix=next_prefix) def _tree_summary(self): amount_in_rd = len(self.migration_status.ids_in_rd) amount_pending = len(self.migration_status.ids_pending) if not self.no_blue_ids: amount_in_work = len(self.migration_status.ids_in_progress) amount_released = amount_in_rd - amount_in_work progress = int(amount_in_rd / len(self.asm_bom_data.child_ids) * 100) column_width = 5 border_padding = "".ljust(column_width, "─") empty_padding = "".ljust(column_width, " ") red_entry = f"│{Treeview.console_colors['RED'] } {Treeview.console_colors['RESET']}│ Migration ausstehend │ {str(amount_pending).ljust(column_width)}│" if not self.no_blue_ids: blue_entry = f"│{Treeview.console_colors['BLUE'] } {Treeview.console_colors['RESET']}│ PDM-Migration abgeschlossen │ {str(amount_in_work).ljust(column_width)}│\n" green_entry = f"│{Treeview.console_colors['GREEN']} {Treeview.console_colors['RESET']}│ Migration abgeschlossen (Freigegeben) │ {str(amount_released).ljust(column_width)}│\n" else: blue_entry = "" green_entry = f"│{Treeview.console_colors['GREEN']} {Treeview.console_colors['RESET']}│ Migration abgeschlossen │ {str(amount_in_rd).ljust(column_width)}│\n" red_entry = f"│{Treeview.console_colors['RED'] } {Treeview.console_colors['RESET']}│ Migration ausstehend │ {str(amount_pending).ljust(column_width)}│" header = f""" ╭───────────────────────────────────────────{border_padding}╮ │ Auswertung {empty_padding}│ ├─┬───────────────────────────────────────┬─{border_padding}┤ """ progress = f""" ├─┴───────────────────────────────────────┼─{border_padding}┤ │ Migrationsfortschritt │ {f"{progress}%".ljust(column_width)}│ ╰─────────────────────────────────────────┴─{border_padding}╯ """ print(header + green_entry + blue_entry + red_entry + progress) def _print_entry(self, child_id, prefix, connector, color): """Eintrag in die Konsole drucken""" text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(self.longest_entry) description = self.asm_bom_data.get_bom_property_values("Bezeichnung")[child_id] print(f"{text_before_description}{description}") def _last_child_check(self, child_index, children): """Prüft, ob aktuelle ID das letzte Kind der Baugruppe ist""" return True if child_index == len(children) - 1 else False def _set_connector_symbol(self, child_is_last_child): """Legt Verbindungssymbol für aktuellen Eintrag im Baum fest""" return Treeview.connector_symbols["ELBOW"] if child_is_last_child else Treeview.connector_symbols["T"] def _prefix(self, prefix, child_is_last_child): """Erstellt für jeden Eintrag im Baum einen Präfix (Teil vor dem Verbindersymbol) fest""" prefix += Treeview.prefix_types["EMPTY"] if child_is_last_child else Treeview.prefix_types["PIPE"] return prefix def _set_text_background_color(self, id): """Legt Hintergrundfarbe der Teilenummer basierend auf ihrem Migrationsstatus fest: - grün: Migration vollständig abgeschlossen und freigegeben - blau: Migration ins PDM abgeschlossen - rot: Migration ausstehend""" if id in self.migration_status.ids_pending: return Treeview.console_colors["RED"] if not self.no_blue_ids: if id in self.migration_status.ids_in_progress: return Treeview.console_colors["BLUE"] return Treeview.console_colors["GREEN"] if __name__ == '__main__': parser = argparse.ArgumentParser(description='Programm gibt Auskunft über den aktuellen Migrationsstand', prog='Display Migration Status') parser.add_argument('-s', '--sivasid', 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='Visualisiert Baugruppenstruktur als Baumstruktur mit farbiger Hervorhebung der Stücklistenpositionen in Abhängigkeit vom Migrationsstatus') parser.add_argument('-e', '--excel', action='store_true', help='Exportiert Baugruppenstruktur als Excel-Datei mit farbiger Hervorhebung der Stücklistenpositionen, in Abhängigkeit vom Migrationsstatus') parser.add_argument('-rd_ids_filedir', action='store', default=os.environ.get("CREMIG_DATA"), help='Legt das Verzeichnis fest, in dem die CSV-Datei liegt, in der die RuleDesigner-Teilenummern aufgelistet sind. Default: Umgebungsvariable %CREMIG_DATA%') parser.add_argument('-rd_ids_filename', action='store', default=os.environ.get("RD_DATABASE_NAME"), help="Name der CSV-Datei, in der die RuleDesigner-Teilenummern aufgelistet sind. Default: Umgebungsvariable %RD_DATABASE_NAME%") parser.add_argument('-rd_ids_local', action='store_true', default=False, help='Legt fest, ob die CSV, die die RuleDesigner-Teilenummern enthält (RD-Teilenummer.csv) bei jedem Programmlauf vom Server in das lokale Verzeichnis kopiert werden soll. Default: Kopieren bei jedem Programmlauf') parser.add_argument('-asm_json_filedir', action='store', default=os.environ.get("CREMIG_DATA"), help='Legt das Verzeichnis fest, in dem die JSON-Datei mit den Baugruppeninformationen liegt. Default: Umgebungsvariable %CREMIG_DATA%', metavar='directory') parser.add_argument('-asm_json_local', action='store_true', default=False, help='Legt fest, ob die JSON der Baugruppe bei jedem Programmlauf neu geladen wird, oder ob die lokale Datei eingelesen werden soll. Default: Neu laden bei jedem Programmlauf') parser.add_argument('-asm_csv_filedir', action='store', default=os.environ.get("CREMIG_DATA"), help='Legt das Verzeichnis fest, in dem die CSV-Datei mit den Teilestamm-Metadaten der Baugruppennummer liegt. Default: Umgebungsvariable %CREMIG_DATA%', metavar='directory') parser.add_argument('-asm_csv_local', action='store_true', default=False, help='Legt fest, ob die Metadaten-CSV der Baugruppennummer bei jedem Programmlauf neu geladen wird. Default: Neu laden bei jedem Programmlauf') parser.add_argument('-no_blue_ids', action='store_true', default=False, help='Legt fest, ob blaue IDs im Baum angezeigt werden sollen. Erforderlich, wenn keine Verbindung zu RDServer besteht, da SQL-Abfrage durchgeführt werden muss') args = parser.parse_args() asm_id = args.sivasid.lstrip() asm_bom_filename = f'{asm_id}.json' asm_metadata_filename = f'{asm_id}.csv' if not args.no_blue_ids: if not os.path.isfile(os.environ.get("RD_SQL_DATABASE_CREDENTIALS")): print(f"ACHTUNG: Blau hinterlegte Nummern (Teilenummern die ins PDM migriert wurden, aber aktuell nicht freigegeben sind) werden nicht dargestellt, da nicht auf die Login-Daten für die RD-SQL-Datenbank zugegriffen werden kann. Bitte prüfen, ob die Login-Datei '{os.environ.get("RD_SQL_DATABASE_CREDENTIALS")}' existiert. Auch prüfen, ob die Netzlaufwerke verbunden sind!") args.no_blue_ids = True if args.excel: excel_comparison = ExcelExport(asm_id=asm_id, asm_json_dir=args.asm_json_filedir, asm_json_filename=asm_bom_filename, asm_csv_dir=args.rd_ids_filedir, asm_csv_filename=asm_metadata_filename, rd_dbase_dir=args.rd_ids_filedir, rd_dbase_filename=args.rd_ids_filename, load_local_asm_json=args.asm_json_local, load_local_asm_csv=args.asm_csv_local, load_local_rd_dbase=args.asm_csv_local, no_blue_ids=args.no_blue_ids) excel_comparison.write_excel() elif args.tree: treeview_comparison = Treeview(asm_id=asm_id, asm_json_dir=args.asm_json_filedir, asm_json_filename=asm_bom_filename, asm_csv_dir=args.rd_ids_filedir, asm_csv_filename=asm_metadata_filename, rd_dbase_dir=args.rd_ids_filedir, rd_dbase_filename=args.rd_ids_filename, load_local_asm_json=args.asm_json_local, load_local_asm_csv=args.asm_csv_local, load_local_rd_dbase=args.rd_ids_local, no_blue_ids=args.no_blue_ids) treeview_comparison.generate_tree() else: parser.print_help()