Files
CreoMigrate/lib/compare_lists.py
T

398 lines
16 KiB
Python

from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig
import warnings
#Wegen Pandas Deprecation eingebaut
warnings.filterwarnings("ignore", category=DeprecationWarning)
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 MigrationStatus:
def __init__(self, asm_child_ids: tuple, rd_ids: tuple):
self.asm_child_ids = asm_child_ids
self.rd_ids = rd_ids
self._overview = None
self._green_ids = None
self._yellow_ids = None
self._red_ids = None
@property
def overview(self) -> dict:
"""Returns dict with children of the given assembly and their migration status (True = migration complete, False = migration pending)"""
if self._overview is None:
self._overview = {}
for child_id in self.asm_child_ids:
self._overview[child_id] = True if child_id in self.rd_ids else False
return self._overview
@property
def green_ids(self) -> list:
"""Returns migrated child-ids of given assembly"""
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
@property
def red_ids(self) -> list:
"""Returns unmigrated child-ids of given assembly"""
if self._red_ids is None:
self._red_ids = [id for id, status in self.overview.items() if status is False]
return self._red_ids
@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:
def __init__(self, asm_id, asm_json_dir, asm_json_filename, rd_dbase_dir, rd_dbase_filename):
self.asm_id = asm_id
self._asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename)
self._rd_dbase = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename)
self._asm_child_ids = None
self._rd_ids = None
self._migration_status = None
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):
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
# Headers
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
# Values below Headers
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
# 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 vba-macro, can't be performed
# when no data is in the spreadsheet
worksheet.title = "Ergebnis"
workbook.save(out_path)
print(f"Excel-Datei erfolgreich in '{out_dir}' geschrieben...")
def _create_excel_data(self) -> dict:
# Columns in excel-spreadsheet: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung
# Sortierung: Minus-separated hierarchy of the partnumber in the BOM structure
# Teilenummer: SIVAS-partnumber
# Bezeichnung: SIVAS-description of the partnumber
# Migrationsstatus: "IMPORTIEREN" = partnumber hasn't been migrated yet, "VERFÜGBAR" = partnumber has been migrated
# All the other columns remain empty, because their values are entered in the excel spreadsheet
sorting = self._create_sorting_string()
migrated = self.migration_status.green_ids
descriptions = self.id_descriptions
prt_num = []
description = []
migration_status = []
evaluation = []
comment = []
for partnumber_string in sorting:
partnumber = partnumber_string.split("-")[-1]
prt_num.append(partnumber)
if partnumber not in descriptions:
description.append("")
else:
description.append(descriptions[partnumber])
migration_status.append("VERFÜGBAR" if partnumber in migrated else "IMPORTIEREN")
evaluation.append('')
comment.append('')
data = {
"Sortierung": sorting,
"Teilenummer": prt_num,
"Bezeichnung": description,
"Migrationsstatus": migration_status,
"Beurteilung": evaluation,
"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]
children = self.parent_children[parent]
for child in children:
sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-"))
if child in self.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):
self.asm_id = asm_id
self._asm_data = SivasAssemblyData(asm_json_dir, asm_json_filename)
self._rd_dbase = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename)
self._asm_child_ids = None
self._rd_ids = None
self._migration_status = None
self._parent_children = None
self._id_descriptions = None
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):
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
children_ids = self.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 self._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_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.max_tree_depth * len(Treeview.prefix_types["PIPE"]) \
+ len(connector) \
+ len(self.longest_partnumber) \
+ 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.id_descriptions[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.overview[partnumber] is True else Treeview.console_colors["RED"]
if __name__ == '__main__':
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('-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('-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()
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)
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)
treeview_comparison.generate_tree()
else:
parser.print_help()