337 lines
16 KiB
Python
337 lines
16 KiB
Python
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 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,
|
|
asm_csv_dir,
|
|
asm_csv_filename,
|
|
rd_dbase_dir,
|
|
rd_dbase_filename,
|
|
load_local_asm_json,
|
|
load_local_asm_csv,
|
|
load_local_rd_dbase):
|
|
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._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):
|
|
|
|
# Längster Eintrag wird zur Bestimmung der Leerraum-Breite zwischen Teilenummer und Bezeichnung im Baum benötigt
|
|
# Es hätte für die Berechnung sowohl ein anderer Farbcode, als auch ein anderes Verbindersymbol angegeben werden können,
|
|
# da die Anderen gleich lang wie die Angegebenen sind
|
|
|
|
if self._longest_entry is None:
|
|
self._longest_entry = self.asm_bom_data.max_bom_depth * len(Treeview.prefix_types["PIPE"]) \
|
|
+ len(self.asm_bom_data.longest_child_id) \
|
|
+ len(Treeview.connector_symbols["T"]) \
|
|
+ len(Treeview.console_colors["RESET"]) \
|
|
+ len(Treeview.console_colors["RED"])
|
|
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_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_bom_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"""
|
|
|
|
|
|
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):
|
|
|
|
"""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('-asmjdir', '--asm_json_dir', required=False, 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('-asmcdir', '--asm_csv_dir', required=False, 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('-lasmjson', '--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('-lasmcsv', '--local_asm_csv', required=False, 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('-lrdcsv', '--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_bom_filename = f'{asm_id}.json'
|
|
asm_metadata_filename = f'{asm_id}.csv'
|
|
|
|
if args.excel:
|
|
excel_comparison = ExcelExport(asm_id=asm_id,
|
|
asm_json_dir=args.asm_json_dir,
|
|
asm_json_filename=asm_bom_filename,
|
|
rd_dbase_dir=args.rd_dbase_dir,
|
|
rd_dbase_filename=args.rd_dbase_filename,
|
|
load_local_asm_json=args.local_asm_json,
|
|
load_local_rd_dbase=args.local_rd_csv)
|
|
excel_comparison.write_excel()
|
|
elif args.tree:
|
|
treeview_comparison = Treeview(asm_id=asm_id,
|
|
asm_json_dir=args.asm_json_dir,
|
|
asm_json_filename=asm_bom_filename,
|
|
asm_csv_dir=args.asm_csv_dir,
|
|
asm_csv_filename=asm_metadata_filename,
|
|
rd_dbase_dir=args.rd_dbase_dir,
|
|
rd_dbase_filename=args.rd_dbase_filename,
|
|
load_local_asm_json=args.local_asm_json,
|
|
load_local_asm_csv=args.local_asm_csv,
|
|
load_local_rd_dbase=args.local_rd_csv)
|
|
|
|
treeview_comparison.generate_tree()
|
|
else:
|
|
parser.print_help() |