Klassen 'Partnumbers', 'SivasPartnumbers' und 'RuleDesignerPartnumbers' entfernt und stattdessen mit den Klassen in manage_configs.py verknüpft, um Redundanzen zu vermeiden. Zudem doppeltes Laden der genannten Klassen in der Klasse 'RuleDesignerComparison', die jetzt 'MigrationStatus' heißt, gefixt. Abgesehen davon durch Aufrufen von Methoden in den __init__-Methoden mittels lazy-Initialisierung per property-Dekoratoren behoben.

This commit is contained in:
2025-03-08 18:16:17 +01:00
parent 5bd83324b2
commit da4eed34c3
+159 -219
View File
@@ -1,12 +1,10 @@
import update_database from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig
import warnings import warnings
#Wegen Pandas Deprecation eingebaut #Wegen Pandas Deprecation eingebaut
warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=DeprecationWarning)
import argparse import argparse
import os import os
import json
import pandas as pd
import openpyxl import openpyxl
""" """
@@ -16,200 +14,100 @@ Dieses Programm kann:
- die oben beschriebene Darstellung in einer Excel-Datei auch als Treeview auf der Kommandozeile darstellen - die oben beschriebene Darstellung in einer Excel-Datei auch als Treeview auf der Kommandozeile darstellen
""" """
class Environment: class MigrationStatus:
def __init__(self): def __init__(self, asm_child_ids: tuple, rd_ids: tuple):
self.lib_dir = os.environ.get('CREMIG_LIB') self.asm_child_ids = asm_child_ids
self.data_dir = os.environ.get('CREMIG_DATA') self.rd_ids = rd_ids
self.out_dir = os.environ.get('CREMIG_WORK') self._overview = None
self.ruledesigner_csv_filename = os.environ.get('RD_DATABASE_NAME') self._green_ids = None
self.vba_macro_filename = os.environ.get('VBA_MACRO_NAME') self._yellow_ids = None
self.vba_macro_filepath = os.path.join(self.lib_dir, self.vba_macro_filename) self._red_ids = None
class Partnumbers:
def __init__(self, filename: str, file_dir: str):
self.file_dir = file_dir
self.filename = filename
def load_from_json(self):
"""Reads partnumbers from a json-file"""
config_path = os.path.join(self.file_dir, self.filename + ".json")
try:
with open(config_path, encoding='utf-8') as json_file:
return json.load(json_file)
except FileNotFoundError:
update_database.get_sivas_dbase(self.filename, self.file_dir)
return self.load_from_json()
def load_from_csv(self):
"""Reads partnumbers from a csv-file"""
config_path = os.path.join(self.file_dir, self.filename)
try:
df = pd.read_csv(config_path, sep=';', converters={"Teilenummern":str})
return tuple(df["Teilenummern"])
except FileNotFoundError:
update_database.get_rd_dbase(self.file_dir)
return self.load_from_csv()
class SivasPartnumbers(Partnumbers):
def __init__(self, main_sivas_id: str, file_dir: str):
self.file_dir = file_dir
self.main_sivas_id = main_sivas_id
self._partnumbers = None
super().__init__(file_dir=self.file_dir, filename=self.main_sivas_id)
def lazy_partnumber_init(function):
"""Decorator prevents time-consuming initialization of sivas partnumbers if method doesn't require them"""
def wrapper(self):
if self._partnumbers is None:
self._partnumbers = self.__class__.mro()[1].load_from_json(self) # Using super() isn't possible here
return function(self)
return wrapper
@lazy_partnumber_init
def get_partnumbers(self) -> set:
"""Returns all partnumbers (id's) of the provided json-file"""
ids = set()
for child_id, top_level_assemblies_of_child in self._partnumbers.items():
ids.add(child_id)
for top_level_assembly in top_level_assemblies_of_child:
top_level_assembly_id = top_level_assembly["Parent"]
ids.add(top_level_assembly_id)
return ids @property
def overview(self) -> dict:
"""Returns dict with children of the given assembly and their migration status (True = migration complete, False = migration pending)"""
def get_longest_partnumber(self) -> str: if self._overview is None:
self._overview = {}
"""Returns the longest partnumber in the provided json-file""" for child_id in self.asm_child_ids:
self._overview[child_id] = True if child_id in self.rd_ids else False
return max(self.get_partnumbers()) return self._overview
@lazy_partnumber_init @property
def get_ordered_parent_children(self, no_duplicates: bool = False) -> dict: def green_ids(self) -> list:
"""Replicates the partnumber structure of the given BOM (bill of material) from sivas. The Order of the partnumbers is identical to the position numbers in the BOM""" """Returns migrated child-ids of given assembly"""
unordered_parent_children = {} if self._green_ids is None:
self._green_ids = [id for id, status in self.overview.items() if status is True]
return self._green_ids
for parents in self._partnumbers: @property
for children in self._partnumbers[parents]: def red_ids(self) -> list:
if children["Parent"] not in unordered_parent_children: """Returns unmigrated child-ids of given assembly"""
unordered_parent_children[children["Parent"]] = {}
pos = children["Position"].lstrip(".").split("/")[0] if self._red_ids is None:
pos_in_list = int(pos) self._red_ids = [id for id, status in self.overview.items() if status is False]
unordered_parent_children[children["Parent"]][pos_in_list] = parents return self._red_ids
# Second dict necessary, because children aren't sorted according to their position numbers in the SIVAS-BOM
ordered_parent_children = {}
for key, values in unordered_parent_children.items():
for subkey, subvalue in sorted(values.items()):
if key not in ordered_parent_children:
ordered_parent_children[key] = []
ordered_parent_children[key].append(subvalue)
return ordered_parent_children
@lazy_partnumber_init
def get_max_bom_depth(self) -> int:
"""Returns the maximum nesting-depth (level) of the provided assembly partnumber as an integer"""
bom_levels = []
for values in self._partnumbers.values():
for value in values:
current_bom_level = value["Position"].count(".")
bom_levels.append(current_bom_level)
return max(bom_levels)
@lazy_partnumber_init
def get_partnumber_descriptions(self) -> dict:
descriptions = {}
for parent, values in self._partnumbers.items():
for value in values:
descriptions[parent] = value["Bezeichnung"]
return descriptions
class RuleDesignerPartnumbers(Partnumbers):
def __init__(self, ruledesigner_ids_dir: str, ruledesigner_ids_filename: str):
self.ruledesigner_ids_dir = ruledesigner_ids_dir
self.ruledesigner_ids_filename = ruledesigner_ids_filename
super().__init__(file_dir=self.ruledesigner_ids_dir, filename=self.ruledesigner_ids_filename)
def get_partnumbers(self):
return super().load_from_csv()
def update_database(self):
return update_database.get_rd_dbase(self.ruledesigner_ids_dir)
class SivasRuleDesignerComparison:
def __init__(self, sivas_ids_dir, sivas_main_id, rd_ids_dir, rd_ids_filename):
self.sivas = SivasPartnumbers(main_sivas_id=sivas_main_id, file_dir=sivas_ids_dir)
self.ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_ids_dir, ruledesigner_ids_filename=rd_ids_filename)
def get_migration_status(self) -> dict:
"""Checks if given sivas-partnumbers have been migrated been migrated into RuleDesigner"""
sivas_ids = self.sivas.get_partnumbers()
ruledesigner_ids = self.ruledesigner.get_partnumbers()
migration_completed = {}
for sivas_partnumber in sivas_ids:
migration_completed[sivas_partnumber] = True if sivas_partnumber in ruledesigner_ids else False
return migration_completed
@property
def yellow_ids(self) -> list:
"""Returns migrated child-ids of given assembly, which prevent the asm from being released, because they are currently being modified"""
pass
class ExcelExport: class ExcelExport:
def __init__(self, main_id, sivas_ids_file_dir, rd_ids_file_dir, rd_ids_filename): def __init__(self, asm_id, asm_json_dir, asm_json_filename, rd_dbase_dir, rd_dbase_filename):
self.main_id = main_id self.asm_id = asm_id
self.environment = Environment() self._asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename)
self._sivas = SivasPartnumbers(main_sivas_id=main_id, file_dir=sivas_ids_file_dir) self._rd_dbase = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename)
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_ids_file_dir, ruledesigner_ids_filename=rd_ids_filename) self._asm_child_ids = None
self._comparison = SivasRuleDesignerComparison(sivas_ids_dir=sivas_ids_file_dir, sivas_main_id=main_id, rd_ids_dir=rd_ids_file_dir, rd_ids_filename=rd_ids_filename) self._rd_ids = None
self._parent_children = self._sivas.get_ordered_parent_children() self._migration_status = None
self._ruledesigner_ids = self._ruledesigner.get_partnumbers() self._parent_children = None
self._id_descriptions = None
@property
def rd_ids(self):
if self._rd_ids is None:
self._rd_ids = self._rd_dbase.get_ruledesigner_ids()
return self._rd_ids
@property
def migration_status(self):
if self._migration_status is None:
self._migration_status = MigrationStatus(self.asm_child_ids, self.rd_ids)
return self._migration_status
@property
def parent_children(self) -> dict:
if self._parent_children is None:
self._parent_children = self._asm_data.get_parent_children()
return self._parent_children
@property
def id_descriptions(self):
if self._id_descriptions is None:
self._id_descriptions = self._asm_data.get_bom_property_values("Bezeichnung")
return self._id_descriptions
@property
def asm_child_ids(self):
if self._asm_child_ids is None:
self._asm_child_ids = self._asm_data.get_assembly_ids()
return self._asm_child_ids
def write_excel(self): def write_excel(self):
data = self._create_excel_data() data = self._create_excel_data()
output_dir = environment.out_dir out_dir = os.environ.get("CREMIG_WORK")
out_path = os.path.join(out_dir, self.asm_id + ".xlsm")
destiny_filepath = os.path.join(output_dir, self.main_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)
workbook = openpyxl.load_workbook(self.environment.vba_macro_filepath, keep_vba=True)
worksheet = workbook.active worksheet = workbook.active
# Headers # Headers
@@ -222,13 +120,13 @@ class ExcelExport:
for cell, element in zip(column, value): for cell, element in zip(column, value):
cell.value = element cell.value = element
# Renaming the sheet is necessary, because it triggers the execution of the VBA-Macro # Renaming the sheet is necessary, because that triggers the execution of the VBA-Macro
# The renaming must be performed AFTER inserting the data into the spreadsheet, because the conditional formatting, which is part of the macro, can't be performed # The renaming must be performed after inserting the data into the spreadsheet, because the conditional formatting, which is part of the vba-macro, can't be performed
# when no data is in the spreadsheet # when no data is in the spreadsheet
worksheet.title = "Ergebnis" worksheet.title = "Ergebnis"
workbook.save(destiny_filepath) workbook.save(out_path)
print(f"Excel-Datei erfolgreich in '{output_dir}' geschrieben...") print(f"Excel-Datei erfolgreich in '{out_dir}' geschrieben...")
def _create_excel_data(self) -> dict: def _create_excel_data(self) -> dict:
@@ -240,8 +138,8 @@ class ExcelExport:
# All the other columns remain empty, because their values are entered in the excel spreadsheet # All the other columns remain empty, because their values are entered in the excel spreadsheet
sorting = self._create_sorting_string() sorting = self._create_sorting_string()
migrated = self._comparison.get_migration_status() migrated = self.migration_status.green_ids
descriptions = self._sivas.get_partnumber_descriptions() descriptions = self.id_descriptions
prt_num = [] prt_num = []
description = [] description = []
@@ -256,7 +154,7 @@ class ExcelExport:
description.append("") description.append("")
else: else:
description.append(descriptions[partnumber]) description.append(descriptions[partnumber])
migration_status.append("VERFÜGBAR" if migrated[partnumber] is True else "IMPORTIEREN") migration_status.append("VERFÜGBAR" if partnumber in migrated else "IMPORTIEREN")
evaluation.append('') evaluation.append('')
comment.append('') comment.append('')
@@ -284,21 +182,20 @@ class ExcelExport:
""" """
if parent is None: if parent is None:
parent = self.main_id parent = self.asm_id
if sorting_strings is None: if sorting_strings is None:
sorting_strings = [parent] sorting_strings = [parent]
children = self._parent_children[parent] children = self.parent_children[parent]
for child in children: for child in children:
sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-")) sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-"))
if child in self._parent_children: if child in self.parent_children:
self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings) self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings)
return sorting_strings return sorting_strings
class Treeview: class Treeview:
@@ -319,16 +216,59 @@ class Treeview:
} }
def __init__(self, main_partnumber, sivas_src_file, rd_src_dir, rd_src_file): def __init__(self, asm_id, asm_json_dir, asm_json_filename, rd_dbase_dir, rd_dbase_filename):
self.main_partnumber = main_partnumber self.asm_id = asm_id
self._sivas = SivasPartnumbers(main_sivas_id=main_partnumber, file_dir=sivas_src_file) self._asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename)
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_src_dir, ruledesigner_ids_filename=rd_src_file) self._rd_dbase = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename)
self._sivas_rd_comparison = SivasRuleDesignerComparison(sivas_main_id=main_partnumber, sivas_ids_dir=sivas_src_file, rd_ids_dir=rd_src_dir, rd_ids_filename=rd_src_file) self._asm_child_ids = None
self._parent_children = self._sivas.get_ordered_parent_children() self._rd_ids = None
self._descriptions = self._sivas.get_partnumber_descriptions() self._migration_status = None
self._max_tree_depth = self._sivas.get_max_bom_depth() self._parent_children = None
self._longest_partnumber = self._sivas.get_longest_partnumber() self._id_descriptions = None
self._migration_status = self._sivas_rd_comparison.get_migration_status() self._max_tree_depth = None
self._longest_partnumber = None
@property
def asm_child_ids(self):
if self._asm_child_ids is None:
self._asm_child_ids = self._asm_data.get_assembly_ids()
return self._asm_child_ids
@property
def rd_ids(self):
if self._rd_ids is None:
self._rd_ids = self._rd_dbase.get_ruledesigner_ids()
return self._rd_ids
@property
def migration_status(self):
if self._migration_status is None:
self._migration_status = MigrationStatus(self.asm_child_ids, self.rd_ids)
return self._migration_status
@property
def parent_children(self) -> dict:
if self._parent_children is None:
self._parent_children = self._asm_data.get_parent_children()
return self._parent_children
@property
def id_descriptions(self):
if self._id_descriptions is None:
self._id_descriptions = self._asm_data.get_bom_property_values("Bezeichnung")
return self._id_descriptions
@property
def max_tree_depth(self):
if self._max_tree_depth is None:
self._max_tree_depth = self._asm_data.get_max_bom_depth()
return self._max_tree_depth
@property
def longest_partnumber(self):
if self._longest_partnumber is None:
self._longest_partnumber = self._asm_data.get_longest_assembly_id()
return self._longest_partnumber
def generate_tree(self): def generate_tree(self):
self._tree_header() self._tree_header()
@@ -339,17 +279,17 @@ class Treeview:
"""Prints root number (number for which the user wants to see the tree) to the console""" """Prints root number (number for which the user wants to see the tree) to the console"""
background_color = self._set_text_background_color(self.main_partnumber) background_color = self._set_text_background_color(self.asm_id)
print(f"{background_color}{self.main_partnumber}{Treeview.console_colors['RESET']}") print(f"{background_color}{self.asm_id}{Treeview.console_colors['RESET']}")
def _tree_body(self, parent_id=None, prefix=""): def _tree_body(self, parent_id=None, prefix=""):
"""Prints the body (all partnumbers except root/main-partnumber) of the tree to the console""" """Prints the body (all partnumbers except root/main-partnumber) of the tree to the console"""
if parent_id is None: if parent_id is None:
parent_id = self.main_partnumber parent_id = self.asm_id
children_ids = self._parent_children[parent_id] children_ids = self.parent_children[parent_id]
for index, child_id in enumerate(children_ids): for index, child_id in enumerate(children_ids):
@@ -366,11 +306,10 @@ class Treeview:
def _tree_summary(self): def _tree_summary(self):
amount_all_sivas_ids = len(self._migration_status) amount_migrated_ids = len(self.migration_status.green_ids)
amount_migrated_ids = sum(migration_status == True for migration_status in self._migration_status.values()) amount_unmigrated_ids = len(self.migration_status.red_ids)
amount_unmigrated_ids = amount_all_sivas_ids - amount_migrated_ids
progress = int(amount_migrated_ids / amount_all_sivas_ids * 100) progress = int(amount_migrated_ids / len(self.asm_child_ids) * 100)
column_width = 5 column_width = 5
border_padding = "".ljust(column_width, "") border_padding = "".ljust(column_width, "")
@@ -392,14 +331,14 @@ class Treeview:
"""Prints tree entry""" """Prints tree entry"""
# Necessary for calculating the padding-width after the partnumber # Necessary for calculating the padding-width after the partnumber
longest_entry = self._max_tree_depth * len(Treeview.prefix_types["PIPE"]) \ longest_entry = self.max_tree_depth * len(Treeview.prefix_types["PIPE"]) \
+ len(connector) \ + len(connector) \
+ len(self._longest_partnumber) \ + len(self.longest_partnumber) \
+ len(Treeview.console_colors["RESET"]) \ + 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 + 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) text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(longest_entry)
description = self._descriptions[child_id] description = self.id_descriptions[child_id]
print(f"{text_before_description} {description}") print(f"{text_before_description} {description}")
@@ -432,27 +371,28 @@ class Treeview:
- Migration completed: green - Migration completed: green
- Migration pending: red""" - Migration pending: red"""
return Treeview.console_colors["GREEN"] if self._migration_status[partnumber] is True else Treeview.console_colors["RED"] return Treeview.console_colors["GREEN"] if self.migration_status.overview[partnumber] is True else Treeview.console_colors["RED"]
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Das Programm gibt Auskunft über den aktuellen Migrationsstand', prog='compare_lists') parser = argparse.ArgumentParser(description='Das Programm gibt Auskunft über den aktuellen Migrationsstand', prog='compare_lists')
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('-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='Erzeugt Baumdarstellung aller Teilenummern. grün = Teilenummer vorhanden, rot = Migration ausstehend') 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') parser.add_argument('-e', '--excel', action='store_true', help='Erzeugt Excel-Datei in der alle Teilenummern, die migriert werden müssen, aufgelistet sind')
parser.add_argument('-rddir', '--rd_dbase_dir', action='store', default=os.environ.get("CREMIG_DATA"))
parser.add_argument('-rdfn', '--rd_dbase_filename', action='store', default=os.environ.get("RD_DATABASE_NAME"))
parser.add_argument('-asmdir', '--asm_json_dir', action='store', default=os.environ.get("CREMIG_DATA"))
args = parser.parse_args() args = parser.parse_args()
environment = Environment() asm_id = args.sivasid.lstrip()
asm_json_filename = f'{asm_id}.json'
if args.excel: if args.excel:
num = args.sivasid.lstrip() excel_comparison = ExcelExport(asm_id, args.asm_json_dir, asm_json_filename, args.rd_dbase_dir, args.rd_dbase_filename)
excel_comparison = ExcelExport(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename)
excel_comparison.write_excel() excel_comparison.write_excel()
elif args.tree: elif args.tree:
num = args.sivasid.lstrip() treeview_comparison = Treeview(asm_id, args.asm_json_dir, asm_json_filename, args.rd_dbase_dir, args.rd_dbase_filename)
treeview_comparison = Treeview(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename)
treeview_comparison.generate_tree() treeview_comparison.generate_tree()
else: else:
parser.print_help() parser.print_help()