From 22b0cf8afcdc36111a3c56edd0abe1ea540cca9c Mon Sep 17 00:00:00 2001 From: Simon Steuer Date: Sat, 29 Mar 2025 22:19:59 +0100 Subject: [PATCH] =?UTF-8?q?In=20Treeview=20bzw.=20Excel-Export=20des=20Mig?= =?UTF-8?q?rations=C3=BCberblicks=20werden=20Nummern=20nun=20blau=20markie?= =?UTF-8?q?rt,=20wenn=20diese=20zwar=20in=20RuleDesigner=20migriert=20wurd?= =?UTF-8?q?en,=20dort=20aber=20nocht=20keine=20Freigabe=20erfolgt=20ist.?= =?UTF-8?q?=20Hierf=C3=BCr=20wird=20der=20RuleDesigner-Status=20der=20Numm?= =?UTF-8?q?ern=20live=20aus=20der=20RuleDesigner-Datenbank=20abgerufen.=20?= =?UTF-8?q?Dadurch=20ergibt=20sich=20ein=20detaillierterer=20=C3=9Cberblic?= =?UTF-8?q?k=20=C3=BCber=20den=20aktuellen=20Migrationsstand,=20da=20der?= =?UTF-8?q?=20Gro=C3=9Fteil=20der=20Migrationsarbeit=20erst=20nach=20der?= =?UTF-8?q?=20Migration=20ins=20PDM=20erfolgt=20(Aufbereitung=20der=20Mode?= =?UTF-8?q?lle,=20Zusammenbau=20der=20Baugruppe,=20etc.).=20Abgesehen=20da?= =?UTF-8?q?von=20wird=20die=20Bezeichnung=20der=20obersten=20Baugruppe=20j?= =?UTF-8?q?etzt=20ebenfalls=20im=20Migrations=C3=BCberblick=20angezeigt,?= =?UTF-8?q?=20was=20bisher=20nicht=20der=20Fall=20war.=20Hierf=C3=BCr=20wi?= =?UTF-8?q?rd=20eine=20CSV-Datei=20des=20SIVAS-Teilestamms=20zur=20Baugrup?= =?UTF-8?q?pennummer=20abgeleitet=20daraus=20'Bezeichnung=201'=20ausgelese?= =?UTF-8?q?n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/04_compare_lists.bat | 1 - bin/cremig_setenv.bat | 4 + cfg/RuleDesignerStatusnamen.cfg | 16 +++ lib/compare_lists.py | 190 +++++++++++++++++++++----------- lib/controller.py | 4 +- lib/manage_configs.py | 108 +++++++++++++++--- lib/vba_format_excel.xlsm | Bin 27648 -> 26129 bytes 7 files changed, 240 insertions(+), 83 deletions(-) create mode 100644 cfg/RuleDesignerStatusnamen.cfg diff --git a/bin/04_compare_lists.bat b/bin/04_compare_lists.bat index 22f554e..3840486 100644 --- a/bin/04_compare_lists.bat +++ b/bin/04_compare_lists.bat @@ -1,5 +1,4 @@ @echo off -CALL 01_get_rd_dbase.bat CALL manage_interpreter.bat activate_interpreter python %CREMIG_LIB%\compare_lists.py %* CALL manage_interpreter.bat deactivate_interpreter diff --git a/bin/cremig_setenv.bat b/bin/cremig_setenv.bat index e6bbf79..7510061 100644 --- a/bin/cremig_setenv.bat +++ b/bin/cremig_setenv.bat @@ -32,7 +32,11 @@ set SIVAS_TEILESTAMM=\\195.243.223.3\sivas\jit\programme\KSbExcelExportSivasTeil set SIVAS_EXCEL_EXPORT_DIR=%CREMIG_DATA% set SIEMENS_DATA_PREP_TOOL=C:\Program Files\Siemens\Solid Edge 2023\Program\DataPrepUtility.exe set LOG_EDITOR=notepad.exe +set RD_SQL_DATABASE_CREDENTIALS=\\fileserver01\abteilungen\Entwicklung\Administration\Lizenzen\RDSQLDatabaseCredentials.bat +if exist %RD_SQL_DATABASE_CREDENTIALS% ( + call %RD_SQL_DATABASE_CREDENTIALS% +) rem Um den Python-Interpreter auf U:\ zu nutzen, Kommentar (rem) in nachfolgender Zeile entfernen rem set NETWORK_INTERPRETER_PATH=\\ssg.local\freigaben\Allgemein\Programmierumgebung\Python312 diff --git a/cfg/RuleDesignerStatusnamen.cfg b/cfg/RuleDesignerStatusnamen.cfg new file mode 100644 index 0000000..0431f30 --- /dev/null +++ b/cfg/RuleDesignerStatusnamen.cfg @@ -0,0 +1,16 @@ +[Status] +0=VERFÜGBAR +1=IN BEARBEITUNG +2=IN ÜBERARBEITUNG +3=FREIGEGEBEN +4=FREIGEGEBEN +10=VORLÄUFIG +11=PROTOTYP +12=IN PRÜFUNG +13=UNGÜLTIG +101=BAGATELLÄNDERUNG +102=REVISION VERFÜGBAR +103=ZUR PRÜFUNG +104=REVISION IN PRÜFUNG +105=REVISION ZUR PRÜFUNG +106=GESPERRT diff --git a/lib/compare_lists.py b/lib/compare_lists.py index c650e8d..0c18b85 100644 --- a/lib/compare_lists.py +++ b/lib/compare_lists.py @@ -1,5 +1,4 @@ from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus, TeilestammMetadata - import argparse import os import openpyxl @@ -8,7 +7,7 @@ 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 +- die oben Darstellung in der Excel-Datei als Baum in der Konsole ausgeben """ class ExcelExport: @@ -17,12 +16,17 @@ class ExcelExport: 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_rd_dbase): + load_local_asm_csv, + load_local_rd_dbase, + no_blue_ids): self.asm_id = asm_id self.asm_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 @@ -70,8 +74,12 @@ class ExcelExport: # 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") + migrated_ids = self.migration_status.ids_in_rd + child_descriptions:dict = self.asm_data.get_bom_property_values("Bezeichnung") + parent_description:str = self.asm_metadata.get_property_values("Bezeichnung1") + + descriptions = child_descriptions.copy() + descriptions[asm_id] = parent_description prt_num = [] description = [] @@ -82,14 +90,23 @@ class ExcelExport: for id_str in sorting: id = id_str.split("-")[-1] prt_num.append(id) + assessment.append('') + comment.append('') + 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('') - + + if id in migrated_ids: + if id in self.migration_status.ids_in_progress: + migration_status.append("PDM-Migration abgeschlossen") + else: + migration_status.append("Migration abgeschlossen (freigegeben)") + + else: + migration_status.append("Migration ausstehend") + data = { "Sortierung": sorting, "Teilenummer": prt_num, @@ -145,6 +162,7 @@ class Treeview: console_colors = {"RED": "\033[41m", "GREEN": "\033[42m", + "BLUE": "\033[44m", "RESET": "\033[0m" } @@ -159,11 +177,13 @@ class Treeview: rd_dbase_filename, load_local_asm_json, load_local_asm_csv, - load_local_rd_dbase): + load_local_rd_dbase, + no_blue_ids): 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.no_blue_ids = no_blue_ids self._migration_status = None self._longest_entry = None @@ -176,16 +196,20 @@ class Treeview: @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"]) + longest_color_code = max(Treeview.console_colors.values(), key=len) + longest_connector_symbol = max(Treeview.connector_symbols.values(), key=len) + + padding_after_id = 2 + + if self._longest_entry is None: + + self._longest_entry = (self.asm_bom_data.max_bom_depth) * len(Treeview.prefix_types["PIPE"]) \ + + len(longest_connector_symbol) \ + + len(longest_color_code) \ + + len(self.asm_bom_data.longest_child_id) \ + + len(Treeview.console_colors["RESET"]) \ + + padding_after_id return self._longest_entry def generate_tree(self): @@ -203,7 +227,7 @@ class Treeview: 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}") + print(f"{text_before_description}{description}") def _tree_body(self, parent_id=None, prefix=""): @@ -230,66 +254,96 @@ class Treeview: def _tree_summary(self): - amount_migrated_ids = len(self.migration_status.green_ids) - amount_unmigrated_ids = len(self.migration_status.red_ids) + amount_in_rd = len(self.migration_status.ids_in_rd) + amount_pending = len(self.migration_status.ids_pending) - progress = int(amount_migrated_ids / len(self.asm_bom_data.child_ids) * 100) + if not self.no_blue_ids: + amount_in_work = len(self.migration_status.ids_in_progress) + amount_released = amount_in_rd - amount_in_work + + progress = int(amount_in_rd / 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}╯ -""") + + red_entry = f"│{Treeview.console_colors['RED'] } {Treeview.console_colors['RESET']}│ Migration ausstehend │ {str(amount_pending).ljust(column_width)}│" + + if not self.no_blue_ids: + blue_entry = f"│{Treeview.console_colors['BLUE'] } {Treeview.console_colors['RESET']}│ PDM-Migration abgeschlossen │ {str(amount_in_work).ljust(column_width)}│\n" + green_entry = f"│{Treeview.console_colors['GREEN']} {Treeview.console_colors['RESET']}│ Migration abgeschlossen (Freigegeben) │ {str(amount_released).ljust(column_width)}│\n" + + else: + blue_entry = "" + green_entry = f"│{Treeview.console_colors['GREEN']} {Treeview.console_colors['RESET']}│ Migration abgeschlossen │ {str(amount_in_rd).ljust(column_width)}│\n" + red_entry = f"│{Treeview.console_colors['RED'] } {Treeview.console_colors['RESET']}│ Migration ausstehend │ {str(amount_pending).ljust(column_width)}│" + + + + header = f""" +╭───────────────────────────────────────────{border_padding}╮ +│ Auswertung {empty_padding}│ +├─┬───────────────────────────────────────┬─{border_padding}┤ +""" + + progress = f""" +├─┴───────────────────────────────────────┼─{border_padding}┤ +│ Migrationsfortschritt │ {f"{progress}%".ljust(column_width)}│ +╰─────────────────────────────────────────┴─{border_padding}╯ +""" + + print(header + green_entry + blue_entry + red_entry + progress) def _print_entry(self, child_id, prefix, connector, color): - """Prints tree entry""" + """Eintrag in die Konsole drucken""" 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}") + 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""" + """Prüft, ob aktuelle ID das letzte Kind der Baugruppe ist""" 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""" + """Legt Verbindungssymbol für aktuellen Eintrag im Baum fest""" 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""" + """Erstellt für jeden Eintrag im Baum einen Präfix (Teil vor dem Verbindersymbol) fest""" prefix += Treeview.prefix_types["EMPTY"] if child_is_last_child else Treeview.prefix_types["PIPE"] return prefix - def _set_text_background_color(self, partnumber): + def _set_text_background_color(self, id): - """Sets background color of partnumber in console depending on its migration status - - Migration completed: green - - Migration pending: red""" + """Legt Hintergrundfarbe der Teilenummer basierend auf ihrem Migrationsstatus fest: - return Treeview.console_colors["GREEN"] if self.migration_status.id_status[partnumber] is True else Treeview.console_colors["RED"] + - grün: Migration vollständig abgeschlossen und freigegeben + - blau: Migration ins PDM abgeschlossen + - rot: Migration ausstehend""" + + if id in self.migration_status.ids_pending: + return Treeview.console_colors["RED"] + + if not self.no_blue_ids: + if id in self.migration_status.ids_in_progress: + return Treeview.console_colors["BLUE"] + + return Treeview.console_colors["GREEN"] if __name__ == '__main__': @@ -297,13 +351,14 @@ if __name__ == '__main__': 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') + parser.add_argument('-rd_ids_filedir', 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('-rd_ids_filename', 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('-rd_ids_local', 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') + parser.add_argument('-asm_json_filedir', 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('-asm_json_local', 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('-asm_csv_filedir', 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('-asm_csv_local', 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('-no_blue_ids', action='store_true', default=False, help='Legt fest, ob blaue IDs im Baum angezeigt werden sollen. Erforderlich, wenn keine Verbindung zu RDServer besteht, da SQL-Abfrage durchgeführt werden muss') args = parser.parse_args() @@ -311,26 +366,37 @@ if __name__ == '__main__': asm_bom_filename = f'{asm_id}.json' asm_metadata_filename = f'{asm_id}.csv' + if not args.no_blue_ids: + if not os.path.isfile(os.environ.get("RD_SQL_DATABASE_CREDENTIALS")): + print(f"ACHTUNG: Blau hinterlegte Nummern (Teilenummern die ins PDM migriert wurden, aber aktuell nicht freigegeben sind) werden nicht dargestellt, da nicht auf die Login-Daten für die RD-SQL-Datenbank zugegriffen werden kann. Bitte prüfen, ob die Login-Datei '{os.environ.get("RD_SQL_DATABASE_CREDENTIALS")}' existiert. Auch prüfen, ob die Netzlaufwerke verbunden sind!") + args.no_blue_ids = True + if args.excel: excel_comparison = ExcelExport(asm_id=asm_id, - asm_json_dir=args.asm_json_dir, + asm_json_dir=args.asm_json_filedir, 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) + asm_csv_dir=args.rd_ids_filedir, + asm_csv_filename=asm_metadata_filename, + rd_dbase_dir=args.rd_ids_filedir, + rd_dbase_filename=args.rd_ids_filename, + load_local_asm_json=args.asm_json_local, + load_local_asm_csv=args.asm_csv_local, + load_local_rd_dbase=args.asm_csv_local, + no_blue_ids=args.no_blue_ids) + excel_comparison.write_excel() elif args.tree: treeview_comparison = Treeview(asm_id=asm_id, - asm_json_dir=args.asm_json_dir, + asm_json_dir=args.asm_json_filedir, asm_json_filename=asm_bom_filename, - asm_csv_dir=args.asm_csv_dir, + asm_csv_dir=args.rd_ids_filedir, 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) + rd_dbase_dir=args.rd_ids_filedir, + rd_dbase_filename=args.rd_ids_filename, + load_local_asm_json=args.asm_json_local, + load_local_asm_csv=args.asm_csv_local, + load_local_rd_dbase=args.rd_ids_local, + no_blue_ids=args.no_blue_ids) treeview_comparison.generate_tree() else: diff --git a/lib/controller.py b/lib/controller.py index c8b61fe..9ee4678 100644 --- a/lib/controller.py +++ b/lib/controller.py @@ -126,12 +126,12 @@ class Controller(Observer): if not load_local_asm_json: try: - update_database.get_sivas_teilestamm(migration_status.red_ids, data_dir) + update_database.get_sivas_teilestamm(migration_status.ids_pending, data_dir) except: self.message_error('Abfrage hat nicht funktioniert', 'sivas_db_error') return - self._model.set_red_ids(migration_status.red_ids) + self._model.set_red_ids(migration_status.ids_pending) self._model.set_parent_children(asm_data.parent_children) self._model.set_asm_id(asm_id) self._model.set_bom_properties(asm_data.all_bom_properties) diff --git a/lib/manage_configs.py b/lib/manage_configs.py index 62c891a..81be8f9 100644 --- a/lib/manage_configs.py +++ b/lib/manage_configs.py @@ -1,5 +1,6 @@ import os import json +import pymssql from configparser import ConfigParser import pandas as pd import update_database @@ -18,9 +19,12 @@ class JsonBasedConfig: def load_json_from_file(self): """lade das json file vom gegeben Pfad in ein json Objekt""" - with open(self._config_path, 'r', encoding='utf-8') as fh: - content = fh.read() - return json.loads(content) + try: + with open(self._config_path, 'r', encoding='utf-8') as fh: + content = fh.read() + return json.loads(content) + except FileNotFoundError: + raise ValueError("Bitte prüfen, ob korrekte Teilenummer, bzw. korrekter Dateiname angegeben wurde.") def from_json(self, json_str): """nimm den Json String zur Initialisierung des Objektes""" @@ -40,6 +44,61 @@ class CSVBasedConfig: return pd.read_csv(self._config_path, sep=";", dtype="object") +class SQLBasedConfig: + def __init__(self, server, user, pw, dbase): + self._server = server + self._user = user + self._pw = pw + self._dbase = dbase + + def query_db(self, sql_statement) -> list: + + """Führt Datenbankabfrage aus und gibt Daten zurück""" + with pymssql.connect(self._server, self._user, self._pw, self._dbase) as conn: + with conn.cursor(as_dict=True) as cursor: + try: + cursor.execute(sql_statement) + except pymssql.ProgrammingError: + print("SQL-Statement konnte nicht verarbeitet werden!") + raise + ret = cursor.fetchall() + return ret + +class RuleDesignerSQLData(SQLBasedConfig): + def __init__(self, + server=os.environ.get("RD_SQL_DATABASE_SERVER"), + user=os.environ.get("RD_SQL_DATABASE_USER"), + pw=os.environ.get("RD_SQL_DATABASE_PW"), + dbase=os.environ.get("RD_SQL_DATABASE_NAME")): + super().__init__(server, user, pw, dbase) + self._rd_statuses = None + + @property + def rd_statuses(self) -> dict: + """Gibt Teilenummer und zugehörigen Status zurück""" + + if self._rd_statuses is None: + + # Datenbankabfrage aller Teilenummern (codCodice) und der zugehörigen Status (codStatus) + sql_statement = "SELECT [codCodice] AS ID, [codStatus] AS STATUS FROM [FUSION_PDM].[dbo].[CODICI]" + sql_data = self.query_db(sql_statement) + + # Jeder Teilenummer den sprechenden Stsatusnamen, statt der internen Statusnummer zuweisen + # Dafür laden der Mapping-Datei für die Statusnummern (z.B. 3 = "FREIGEGEBEN") + rd_status_cfg = IniBasedConfig(os.environ.get("CREMIG_CFG"), "RuleDesignerStatusnamen.cfg") + rd_status_sections = rd_status_cfg.sections() + rd_status_names = rd_status_cfg.get_dict(rd_status_sections[0]) + + + self._rd_statuses = {} + for id_status_dict in sql_data: + id = id_status_dict["ID"] + status = id_status_dict["STATUS"] + self._rd_statuses[id] = rd_status_names[str(status)] + + return self._rd_statuses + + class SivasAssemblyData(JsonBasedConfig): """Auslesen der Assembly-Informationen zu einer angegebenen Baugruppennummer""" @@ -196,10 +255,12 @@ 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._rd_statuses = None self._overview = None - self._green_ids = None - self._red_ids = None - + self._ids_in_rd = None + self._ids_in_progress = None + self._ids_pending = None + @property def id_status(self) -> dict: """Gibt zurück, ob die Stücklistenpositionen der Baugruppe migriert sind, oder nicht""" @@ -208,27 +269,38 @@ class MigrationStatus: 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: + def ids_in_rd(self) -> list: """Gibt alle Stücklistenpositionen zurück, die migriert sind""" - if self._green_ids is None: - self._green_ids = [id for id, status in self.id_status.items() if status is True] - return self._green_ids + if self._ids_in_rd is None: + self._ids_in_rd = [id for id, status in self.id_status.items() if status is True] + return self._ids_in_rd @property - def yellow_ids(self) -> list: - """Gibt migrierte Stücklistenpositionen zurück, die aktuell eine Freigabe der Baugruppe verhindern, da sie sich derzeit in Arbeit befinden""" - pass + def ids_in_progress(self) -> list: + """Gibt migrierte Stücklistenpositionen zurück, die aktuell eine Freigabe der Baugruppe verhindern, da sie sich in Arbeit befinden""" + + + if self._ids_in_progress is None: + + rd_sql_data = RuleDesignerSQLData() + rd_statuses = rd_sql_data.rd_statuses + + self._ids_in_progress = [] + for id in self.ids_in_rd: + if rd_statuses[id] != "FREIGEGEBEN": + self._ids_in_progress.append(id) + return self._ids_in_progress @property - def red_ids(self) -> list: + def ids_pending(self) -> list: """Gibt alle Stücklistenpositionen zurück, die noch nicht migriert sind""" - if self._red_ids is None: - self._red_ids = [id for id, status in self.id_status.items() if status is False] - return self._red_ids + if self._ids_pending is None: + self._ids_pending = [id for id, status in self.id_status.items() if status is False] + return self._ids_pending class TeilestammMetadata(CSVBasedConfig): diff --git a/lib/vba_format_excel.xlsm b/lib/vba_format_excel.xlsm index c03ed3d1428bf4d389c21129bf45424abcbc7457..2c7f83df991a2e990165e58d2c58ec145c504d99 100644 GIT binary patch delta 16136 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 J%lLnM{}0$XK(hb< delta 17683 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