from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus 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 beschriebene Darstellung in einer Excel-Datei auch als Treeview auf der Kommandozeile darstellen """ class ExcelExport: def __init__(self, asm_id, asm_json_dir, asm_json_filename, rd_dbase_dir, rd_dbase_filename, load_local_asm_json, load_local_rd_dbase): self.asm_id = asm_id self.asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename, load_local_asm_json) 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.green_ids descriptions = self.asm_data.get_bom_property_values("Bezeichnung") prt_num = [] description = [] migration_status = [] assessment = [] comment = [] for id_str in sorting: id = id_str.split("-")[-1] prt_num.append(id) if id not in descriptions: description.append("") else: description.append(descriptions[id]) migration_status.append("VERFÜGBAR" if id in migrated_ids else "IMPORTIEREN") assessment.append('') comment.append('') 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", "RESET": "\033[0m" } def __init__(self, asm_id, asm_json_dir, asm_json_filename, rd_dbase_dir, rd_dbase_filename, load_local_asm_json, load_local_rd_dbase): self.asm_id = asm_id self.asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename, load_local_asm_json) 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 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""" background_color = self._set_text_background_color(self.asm_id) print(f"{background_color}{self.asm_id}{Treeview.console_colors['RESET']}") 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_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_migrated_ids = len(self.migration_status.green_ids) amount_unmigrated_ids = len(self.migration_status.red_ids) progress = int(amount_migrated_ids / len(self.asm_data.child_ids) * 100) column_width = 5 border_padding = "".ljust(column_width, "─") empty_padding = "".ljust(column_width, " ") print(f""" ╭─────────────────────────────────────{border_padding}╮ │ Auswertung {empty_padding}│ ├───────────────────────────────────┬─{border_padding}┤ │ Anzahl abgeschlossene Migrationen │ {str(amount_migrated_ids).ljust(column_width)}│ │ Anzahl fehlende Migrationen │ {str(amount_unmigrated_ids).ljust(column_width)}│ ├───────────────────────────────────┼─{border_padding}┤ │ Migrationsfortschritt │ {f"{progress}%".ljust(column_width)}│ ╰───────────────────────────────────┴─{border_padding}╯ """) def _print_entry(self, child_id, prefix, connector, color): """Prints tree entry""" # Necessary for calculating the padding-width after the partnumber longest_entry = self.asm_data.max_bom_depth * len(Treeview.prefix_types["PIPE"]) \ + len(connector) \ + len(self.asm_data.longest_child_id) \ + len(Treeview.console_colors["RESET"]) \ + len(Treeview.console_colors["RED"]) # other color codes (ansi escape sequences) could have been used instead since they all have the same length text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(longest_entry) description = self.asm_data.get_bom_property_values("Bezeichnung")[child_id] print(f"{text_before_description} {description}") def _last_child_check(self, child_index, children): """Checks, if current child is the last child of the assembly""" return True if child_index == len(children) - 1 else False def _set_connector_symbol(self, child_is_last_child): """Chooses connector symbol for current tree entry""" return Treeview.connector_symbols["ELBOW"] if child_is_last_child else Treeview.connector_symbols["T"] def _prefix(self, prefix, child_is_last_child): """Creates prefix (part before the connector symbol) for each tree entry""" prefix += Treeview.prefix_types["EMPTY"] if child_is_last_child else Treeview.prefix_types["PIPE"] return prefix def _set_text_background_color(self, partnumber): """Sets background color of partnumber in console depending on its migration status - Migration completed: green - Migration pending: red""" return Treeview.console_colors["GREEN"] if self.migration_status.id_status[partnumber] is True else Treeview.console_colors["RED"] 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('-rddir', '--rd_dbase_dir',required=False, 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('-rdfn', '--rd_dbase_filename', required=False, 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('-asmdir', '--asm_json_dir', required=False, action='store', default=os.environ.get("CREMIG_DATA"), help='Legt das Verzeichnis fest, JSON-Datei mit den Baugruppeninformationen liegen soll. Default: Umgebungsvariable %CREMIG_DATA%', metavar='filename') parser.add_argument('-ljson', '--local_asm_json', required=False, 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('-lcsv', '--local_rd_csv', required=False, 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') args = parser.parse_args() asm_id = args.sivasid.lstrip() asm_json_filename = f'{asm_id}.json' if args.excel: excel_comparison = ExcelExport(asm_id, args.asm_json_dir, asm_json_filename, args.rd_dbase_dir, args.rd_dbase_filename, args.local_asm_json, args.local_rd_csv) excel_comparison.write_excel() elif args.tree: treeview_comparison = Treeview(asm_id, args.asm_json_dir, asm_json_filename, args.rd_dbase_dir, args.rd_dbase_filename, args.local_asm_json, args.local_rd_csv) treeview_comparison.generate_tree() else: parser.print_help()