From 21528e2353d1a5db9faae5aae3b5cbdb5ff11637 Mon Sep 17 00:00:00 2001 From: Simon Steuer Date: Sun, 27 Apr 2025 17:29:09 +0200 Subject: [PATCH] =?UTF-8?q?Basierend=20auf=20Commit=20d905896=20k=C3=B6nne?= =?UTF-8?q?n=20jetzt=20im=20Treeview=20Nummern=20aus=20der=20St=C3=BCcklis?= =?UTF-8?q?tenstruktur=20sowie=20aus=20der=20Zusammenfassung=20ausgeschlos?= =?UTF-8?q?sen=20werden.=20Nummern,=20die=20explizit=20oder=20implizit=20a?= =?UTF-8?q?usgeschlossen=20wurden=20(implizit,=20wenn=20sie=20Bestandteil?= =?UTF-8?q?=20einer=20Baugruppe=20sind,=20die=20ausgeschlossen=20wurde),?= =?UTF-8?q?=20dann=20werden=20diese=20Nummern=20in=20der=20Zusammenfassung?= =?UTF-8?q?=20aufgelistet,=20um=20dem=20User=20zu=20verdeutlichen,=20dass?= =?UTF-8?q?=20die=20Darstellung=20des=20Baumes,=20bzw.=20der=20Zusammenfas?= =?UTF-8?q?sung=20von=20der=20tats=C3=A4chlichen=20Struktur=20abweicht.=20?= =?UTF-8?q?Zur=20Angabe=20der=20ausgeschlossenen=20Nummern=20aus=20dem=20B?= =?UTF-8?q?aum=20k=C3=B6nnen=20die=20neuen=20Schalter=20'--rm=5Fbom=5Fids?= =?UTF-8?q?=5Fstr'=20und=20'--rm=5Fbom=5Fids=5Fcfg'=20genutzt=20werden.=20?= =?UTF-8?q?Bei=20ersterem=20k=C3=B6nnen=20die=20Nummern=20als=20Argumente?= =?UTF-8?q?=20nacheinander=20mit=20Leerzeichen=20getrennt=20angegeben=20we?= =?UTF-8?q?rden.=20Bei=20zweiterem=20Fall=20wird=20eine=20Konfigurationsda?= =?UTF-8?q?tei=20mit=20darin=20definiertem=20Sektionsnamen=20angegeben.=20?= =?UTF-8?q?Analog=20dazu=20gibt=20es=20zum=20Ausschluss=20der=20Teilenumme?= =?UTF-8?q?rn=20aus=20den=20Anzahlen=20der=20Zusammenfassung=20die=20neuen?= =?UTF-8?q?=20Schalter=20'--rm=5Fsummary=5Fids=5Fstr'=20und=20'--rm=5Fsumm?= =?UTF-8?q?ary=5Fids=5Fcfg'.=20Allgemein=20wurde=20auch=20die=20Angabe=20d?= =?UTF-8?q?er=20Argumente=20konkretisiert=20und=20z.B.=20durch=20CustomAct?= =?UTF-8?q?ions=20zur=20Validierung=20der=20Argumente=20verbessert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/compare_lists.py | 217 +++++++++++++++++++++++++++++-------------- 1 file changed, 147 insertions(+), 70 deletions(-) diff --git a/lib/compare_lists.py b/lib/compare_lists.py index 8d460d3..f2a6dde 100644 --- a/lib/compare_lists.py +++ b/lib/compare_lists.py @@ -1,7 +1,8 @@ -from manage_configs import SivasBOM, RuleDesignerDatabaseConfig, MigrationStatus, SivasTeilestamm +from manage_configs import SivasBOM, RuleDesignerDatabaseConfig, MigrationStatus, SivasTeilestamm, BOMConfig import argparse import os import openpyxl +from itertools import zip_longest """ Dieses Programm kann: @@ -10,6 +11,23 @@ Dieses Programm kann: - die oben Darstellung in der Excel-Datei als Baum in der Konsole ausgeben """ +class GetExcludedIDsFromCFG(argparse.Action): + def __call__(self, parser, namespace, values, option_string = None): + bom_config = BOMConfig(os.path.dirname(values[0]), os.path.basename(values[0])) + excluded_ids = bom_config.get_excluded_ids(values[1]) + setattr(namespace, self.dest, excluded_ids) + +class LStrip(argparse.Action): + def __call__(self, parser, namespace, values, option_string = None): + setattr(namespace, self.dest, values.lstrip()) + +class LocateSQLCredentials(argparse.Action): + def __call__(self, parser, namespace, values, option_string = None): + 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!") + setattr(namespace, self.dest, True) + + class ExcelExport: def __init__(self, @@ -168,21 +186,26 @@ class Treeview: 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, - no_blue_ids): + asm_id:str, + asm_json_dir:str, + asm_json_filename:str, + asm_csv_dir:str, + asm_csv_filename:str, + rd_dbase_dir:str, + rd_dbase_filename:bool=False, + load_local_asm_json:bool=False, + load_local_asm_csv:bool=False, + load_local_rd_dbase:bool=False, + no_blue_ids:bool=False, + excluded_bom_ids:list=None, + excluded_summary_ids:list=None): + self.asm_id = asm_id - self.asm_bom = SivasBOM(asm_json_dir, asm_json_filename, load_local_asm_json) + self.asm_bom = SivasBOM(asm_id, asm_json_dir, asm_json_filename, load_local_asm_json, excluded_bom_ids) self.asm_teilestamm = SivasTeilestamm(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.all_excluded_summary_ids = excluded_summary_ids + self._excluded_summary_ids_in_bom = None self.no_blue_ids = no_blue_ids self._migration_status = None self._longest_entry = None @@ -250,17 +273,39 @@ class Treeview: next_prefix = self._set_next_prefix(prefix, id, last_child) self._print_tree(parent_id=id, prefix=next_prefix) + def _remove_ids_from_summary_amount(self, ids: list) -> list: + """ + Reduziert die Anzahl in der Zusammenfassung (z.B. der nicht migrierten Teilenummern), + um die Anzahl an Teilenummern, die vom User aus der Zusammenfassung ausgeschlossen wurden + """ + + if self.all_excluded_summary_ids is None: + return ids + return list(filter(lambda id: id not in self.excluded_summary_ids_in_bom, ids)) + + @property + def excluded_summary_ids_in_bom(self) -> list: + """ Gibt alle Teilenummern zurück, die im Baum vorliegen, aber nicht in der Anzahl der Zusammenfassung enthalten sein sollen""" + + if self._excluded_summary_ids_in_bom is None: + self._excluded_summary_ids_in_bom = list(filter(lambda id: id in self.asm_bom.bom_data, self.all_excluded_summary_ids)) + return self._excluded_summary_ids_in_bom def _print_summary(self): - amount_in_rd = len(self.migration_status.ids_in_rd) - amount_pending = len(self.migration_status.ids_pending) + ids_in_rd = self.migration_status.ids_in_rd + ids_pending = self.migration_status.ids_pending + + amount_in_rd = len(self._remove_ids_from_summary_amount(ids_in_rd)) + amount_pending = len(self._remove_ids_from_summary_amount(ids_pending)) if not self.no_blue_ids: - amount_in_work = len(self.migration_status.ids_in_progress) + ids_in_progress = self.migration_status.ids_in_progress + amount_in_work = len(self._remove_ids_from_summary_amount(ids_in_progress)) amount_released = amount_in_rd - amount_in_work - progress = int(amount_in_rd / len(self.asm_bom.child_ids) * 100) + child_ids = self.asm_bom.child_ids + progress = int(amount_in_rd / len(self._remove_ids_from_summary_amount(child_ids)) * 100) column_width = 5 border_padding = "".ljust(column_width, "─") @@ -285,14 +330,35 @@ class Treeview: │ Auswertung {empty_padding}│ ├─┬───────────────────────────────────────┬─{border_padding}┤ """ - - progress = f""" + progress_segment = f""" ├─┴───────────────────────────────────────┼─{border_padding}┤ │ Migrationsfortschritt │ {f"{progress}%".ljust(column_width)}│ -╰─────────────────────────────────────────┴─{border_padding}╯ """ - print(header + green_entry + blue_entry + red_entry + progress) + bottom_row = f"╰─────────────────────────────────────────┴─{border_padding}╯" + + + exclusion_description = "" + excluded_id_list = "" + + if self.asm_bom.excluded_ids_in_bom or self.excluded_summary_ids_in_bom: + + exclusion_description = f"""├─────────────────────────────────────────┴─{border_padding}┤ +│ {empty_padding}│ +├───────────────────────────────────────────{border_padding}┤ +│ Ausgeschlossene Teilenummern {empty_padding}│ +│──────────────────────┬────────────────────{border_padding}┤ +│ Baum │ Auswertung {empty_padding}│ +├──────────────────────┼────────────────────{border_padding}┤ +""" + + for bom_id, smry_id in zip_longest(self.asm_bom.excluded_ids_in_bom, self.excluded_summary_ids_in_bom, fillvalue=9*" "): + excluded_id_list += f"│ { bom_id} │ {smry_id} {empty_padding}│\n" + + bottom_row = f"╰──────────────────────┴────────────────────{border_padding}╯" + + + print(header + green_entry + blue_entry + red_entry + progress_segment + exclusion_description + excluded_id_list + bottom_row) def _print_entry(self, child_id, prefix, connector, color): @@ -352,58 +418,69 @@ class Treeview: return Treeview.console_colors["GREEN"] - 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('-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') + + def main(): - args = parser.parse_args() + parser = argparse.ArgumentParser(description='Programm gibt Auskunft über den aktuellen Migrationsstand einer SIVAS-Teilenummer', + prog='compare_lists', + formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=50)) # Hilfetext auch bei längeren Argumenten in gleicher Zeile wie Argument darstellen - asm_id = args.sivasid.lstrip() - asm_bom_filename = f'{asm_id}.json' - asm_metadata_filename = f'{asm_id}.csv' + # Custom Actions + parser.register("action", "GetIDsFromCFG", GetExcludedIDsFromCFG) + parser.register("action", "lstrip", LStrip) + parser.register("action", "LocateSQLCredentials", LocateSQLCredentials) - 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 + parser.add_argument('sivas_id', action='lstrip', help='Teilenummer, für die Migrationsüberblick generiert werden soll.', metavar="SIVAS-Teilenummer") + parser.add_argument('output_type', choices=["tree", "excel"], help="'tree' = Darstellung des Überblicks als Baum in der Konsole. 'excel' = Export des Überblicks als Excel") + parser.add_argument('--rd_ids_filepath', action='store', default=os.path.join(os.environ.get("CREMIG_DATA"), "RD_Teilenummern.csv"), help='Verzeichnis der CSV-Datei mit den RuleDesigner-Nummern. Default: $CREMIG_DATA', metavar="filepath") + parser.add_argument('--local_rd_ids', action='store_true', default=False, help='Lokales laden der CSV mit den RuleDesigner-Nummern (RD-Teilenummer.csv) statt Kopieren von Server') + parser.add_argument('--bom_filedir', action='store', default=os.environ.get("CREMIG_DATA"),help='Verzeichnis der JSON-Datei mit den Stücklisteninformationen. Default: $CREMIG_DATA', metavar='dir') + parser.add_argument('--local_bom', action='store_true', default=False, help='Lokales laden der JSON mit den Stücklisteninformationen statt Generierung') + parser.add_argument('--teilestamm_filedir', action='store', default=os.environ.get("CREMIG_DATA"), help='Verzeichnis der CSV mit den SIVAS-Teilestamminformationen. Default: $CREMIG_DATA', metavar='dir') + parser.add_argument('--local_teilestamm', action='store_true', default=False, help='Lokales Laden der CSV mit den Teilestamminformationen statt Generierung') + parser.add_argument('--no_blue_ids', action="store_true", default=False, help='Nummern, die in RD vorliegen, aber nicht freigegeben sind, gleich wie freigegeben Nummern einfärben') - if args.excel: - excel_comparison = ExcelExport(asm_id=asm_id, - asm_json_dir=args.asm_json_filedir, - asm_json_filename=asm_bom_filename, - 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) + exclude_from_overview = parser.add_mutually_exclusive_group() + exclude_from_overview.add_argument('--rm_bom_ids_str', action="store", nargs="*", default=None, dest="excluded_bom_ids", help="Angabe aller Nummern, die vom Migrationsüberblick ausgeschlossen sein sollen", metavar="id1 id2") + exclude_from_overview.add_argument('--rm_bom_ids_cfg', action="GetIDsFromCFG", nargs=2, default=None, dest="excluded_bom_ids", help="cfg-Pfad & Sektion, in der Nummern aufgelistet sind, die nicht im Überblick dargestellt sein sollen", metavar=("path", "section")) - excel_comparison.write_excel() - elif args.tree: - treeview_comparison = Treeview(asm_id=asm_id, - asm_json_dir=args.asm_json_filedir, - asm_json_filename=asm_bom_filename, - 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.rd_ids_local, - no_blue_ids=args.no_blue_ids) - treeview_comparison.generate_tree() - else: - parser.print_help() \ No newline at end of file + exclude_from_summary = parser.add_mutually_exclusive_group() + exclude_from_summary.add_argument('--rm_summary_ids_str', action="store", nargs="*", default=None, dest="excluded_summary_ids", help="Angabe aller Nummern, die von der Zusammenfassung ausgeschlossen sein sollen", metavar="id1 id2") + exclude_from_summary.add_argument('--rm_summary_ids_cfg', action="GetIDsFromCFG", nargs=2, default=None, dest="excluded_summary_ids", help="cfg-Pfad & Sektion, in der Nummern aufgelistet sind, die von der Zusammenfassung ausgeschlossen sein sollen", metavar=("path", "section")) + + args = parser.parse_args() + + match args.output_type: + case "excel": + excel_comparison = ExcelExport(asm_id=args.sivas_id, + asm_json_dir=args.bom_filedir, + asm_json_filename=f'{args.sivas_id}.json', + asm_csv_dir=args.teilestamm_filedir, + asm_csv_filename=f"{args.sivas_id}.csv", + rd_dbase_dir=os.path.dirname(args.rd_ids_filepath), + rd_dbase_filename=os.path.basename(args.rd_ids_filepath), + load_local_asm_json=args.local_bom, + load_local_asm_csv=args.local_teilestamm, + load_local_rd_dbase=args.local_rd_ids, + no_blue_ids=args.no_blue_ids) + + excel_comparison.write_excel() + case "tree": + treeview_comparison = Treeview(asm_id=args.sivas_id, + asm_json_dir=args.bom_filedir, + asm_json_filename=f'{args.sivas_id}.json', + asm_csv_dir=args.teilestamm_filedir, + asm_csv_filename=f'{args.sivas_id}.csv', + rd_dbase_dir=os.path.dirname(args.rd_ids_filepath), + rd_dbase_filename=os.path.basename(args.rd_ids_filepath), + load_local_asm_json=args.local_bom, + load_local_asm_csv=args.local_teilestamm, + load_local_rd_dbase=args.local_rd_ids, + no_blue_ids=args.no_blue_ids, + excluded_bom_ids=args.excluded_bom_ids, + excluded_summary_ids=args.excluded_summary_ids) + + treeview_comparison.generate_tree() + + main() \ No newline at end of file