398 lines
16 KiB
Python
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() |