Compare commits

...

8 Commits

Author SHA1 Message Date
s.steuer 9b3c3859cc get_teilestamm.py wird nicht mehr benötigt, siehe commit 4f740e81ad 2025-03-31 12:53:12 +02:00
s.steuer 22b0cf8afc In Treeview bzw. Excel-Export des Migrationsüberblicks werden Nummern nun blau markiert, wenn diese zwar in RuleDesigner migriert wurden, dort aber nocht keine Freigabe erfolgt ist. Hierfür wird der RuleDesigner-Status der Nummern live aus der RuleDesigner-Datenbank abgerufen. Dadurch ergibt sich ein detaillierterer Überblick über den aktuellen Migrationsstand, da der Großteil der Migrationsarbeit erst nach der Migration ins PDM erfolgt (Aufbereitung der Modelle, Zusammenbau der Baugruppe, etc.). Abgesehen davon wird die Bezeichnung der obersten Baugruppe jetzt ebenfalls im Migrationsüberblick angezeigt, was bisher nicht der Fall war. Hierfür wird eine CSV-Datei des SIVAS-Teilestamms zur Baugruppennummer abgeleitet daraus 'Bezeichnung 1' ausgelesen. 2025-03-29 22:19:59 +01:00
s.steuer e2bfa2aac6 FIX - An erstes Argument (BG-ID) in Methode 'get_sivas_dbase' in 'update_database.py' wurde der komplette Dateipfad zur JSON übergeben, obwohl nur die Teilenummer, bzw. max. die Teilenummer mit Dateiendung übergeben werden darf 2025-03-09 21:40:36 +01:00
s.steuer 5f3615548b Treeview angepasst, damit die Bezeichnung der obersten Baugruppe angezeigt wird, da dies bisher nicht der Fall war. Da die Bezeichnung nicht in der Baugruppen-JSON aufgeführt ist, ist es erforderlich die CSV mit den Teilestamm-Metadaten zur Baugruppennummer herunterzuladen. Damit dies möglich ist, wurde wurde in manage_configs.py eine neue Subclass 'TeilestammMetadata' ergänzt. In compare_lists.py wurden dabei weitere Flags ergänzt, mit denen gesteuert werden kann, ob die Bezeichnung aus der lokalen CSV-Datei gezogen werden sollen, oder ob die CSV mit jedem Programmlauf neu erstellt wird. Defaultmäßig wird sie mit jedem Programmlauf neu erstellt 2025-03-09 21:22:15 +01:00
s.steuer 4f740e81ad Das Einladen der JSON- und RuleDesigner-Daten erfolgt nun allein i.V.m 'manage_configs.py' und nicht mehr i.V.m der Klasse FetchSivasTeilestamm in der Datei 'get_teilestamm.py'. Dies wurde einerseits durchgeführt, um die Anzahl an Referenzen zu verringern, hauptsächlich jedoch, weil in der Klasse 'FetchSivasTeilestamm' die Klasse 'MigrationsStatus' in 'manage_configs.py' aufgerufen wird, in der wiederum Objekte der Klassen 'SivasAssemblyData' und 'RuleDesignerConfig' initiiert werden. Das ist sehr ineffizient, weil 'SivasAssemblyData' bereits in dieser Datei, also 'controller.py' initiiert wird. Es wurde zudem ergänzt, dass nun auch die lokale Version der CSV-Datei, die die RD-Teilenummern enthält geladen werden kann, ohne den aktuellen Stand vom Server zu kopieren. 2025-03-09 00:37:10 +01:00
s.steuer 6f188649ff Großes Refactoring - Dekorator für lazy-Initialisierung durch die in Python für diese Aufgabe vorgesehenen Property-Dekoratoren ersetzt. Durch diese Dekoratoren können zukünftig auch einfach Setter-Methoden ergänzt werden. Das Holen/Laden der JSON- und CSV-Dateien wurde zudem nun so umgebaut, dass dies nicht mehr in der Klasse 'MigrationsStatus' erfolgt, sondern in den Klassen 'SivasAssemblyData' und 'RuleDesignerDatabaseConfig', da dies thematisch dort auch hingehört. Die bisherige Vorgehensweise war einerseits verwirrend und zweitens ineffizient, weil in der Klasse 'MigrationsStatus' nochmal Objekte der Klassen 'SivasAssemblyData' und 'RuleDesignerDatabaseConfig' initialisiert werden mussten. Durch die Anpassung wird nun zudem die Datei 'get_teilestamm.py' nicht länger benötigt 2025-03-09 00:28:09 +01:00
s.steuer beb1fdc0da Klasse MigrationsStatus entfernt und stattdessen auf gleichnamige Klasse in manage_configs.py referenziert, um Redundanzen zu vermeiden. Zudem Flags zur Definierung des JSON-Verzeichnises, des Namens und Verzeichnisses für die CSV mit den RuleDesigner-Teilenummern ergänzt. Außerdem Flags vorgesehen, mit denen gesteuert werden kann, ob die die lokalen JSON-Dateien sowie die lokale CSV der RuleDesigner-Teilenummern verwendet werden soll. Defaultmäßig sollen bei jedem Programmlauf nun immer die neusten Daten vom Server geholt werden, bzw. immer eine JSON generiert werden, da bisher sonst nicht immer die Aktualität der dargestellten Daten sichergestellt war. 2025-03-09 00:16:27 +01:00
s.steuer da4eed34c3 Klassen 'Partnumbers', 'SivasPartnumbers' und 'RuleDesignerPartnumbers' entfernt und stattdessen mit den Klassen in manage_configs.py verknüpft, um Redundanzen zu vermeiden. Zudem doppeltes Laden der genannten Klassen in der Klasse 'RuleDesignerComparison', die jetzt 'MigrationStatus' heißt, gefixt. Abgesehen davon durch Aufrufen von Methoden in den __init__-Methoden mittels lazy-Initialisierung per property-Dekoratoren behoben. 2025-03-08 18:16:17 +01:00
9 changed files with 521 additions and 474 deletions
-1
View File
@@ -1,5 +1,4 @@
@echo off @echo off
CALL 01_get_rd_dbase.bat
CALL manage_interpreter.bat activate_interpreter CALL manage_interpreter.bat activate_interpreter
python %CREMIG_LIB%\compare_lists.py %* python %CREMIG_LIB%\compare_lists.py %*
CALL manage_interpreter.bat deactivate_interpreter CALL manage_interpreter.bat deactivate_interpreter
+4
View File
@@ -32,7 +32,11 @@ set SIVAS_TEILESTAMM=\\195.243.223.3\sivas\jit\programme\KSbExcelExportSivasTeil
set SIVAS_EXCEL_EXPORT_DIR=%CREMIG_DATA% set SIVAS_EXCEL_EXPORT_DIR=%CREMIG_DATA%
set SIEMENS_DATA_PREP_TOOL=C:\Program Files\Siemens\Solid Edge 2023\Program\DataPrepUtility.exe set SIEMENS_DATA_PREP_TOOL=C:\Program Files\Siemens\Solid Edge 2023\Program\DataPrepUtility.exe
set LOG_EDITOR=notepad.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 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 rem set NETWORK_INTERPRETER_PATH=\\ssg.local\freigaben\Allgemein\Programmierumgebung\Python312
+16
View File
@@ -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
+224 -279
View File
@@ -1,271 +1,118 @@
import update_database from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus, TeilestammMetadata
import warnings
#Wegen Pandas Deprecation eingebaut
warnings.filterwarnings("ignore", category=DeprecationWarning)
import argparse import argparse
import os import os
import json
import pandas as pd
import openpyxl import openpyxl
""" """
Dieses Programm kann: Dieses Programm kann:
- eine Excel-Datei exportieren, in der alle Teilenummern aufgelistet sind, die in Verbindung mit einer angegebenen Nummer migriert werden müssen. - 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. 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 Environment:
def __init__(self):
self.lib_dir = os.environ.get('CREMIG_LIB')
self.data_dir = os.environ.get('CREMIG_DATA')
self.out_dir = os.environ.get('CREMIG_WORK')
self.ruledesigner_csv_filename = os.environ.get('RD_DATABASE_NAME')
self.vba_macro_filename = os.environ.get('VBA_MACRO_NAME')
self.vba_macro_filepath = os.path.join(self.lib_dir, self.vba_macro_filename)
class Partnumbers:
def __init__(self, filename: str, file_dir: str):
self.file_dir = file_dir
self.filename = filename
def load_from_json(self):
"""Reads partnumbers from a json-file"""
config_path = os.path.join(self.file_dir, self.filename + ".json")
try:
with open(config_path, encoding='utf-8') as json_file:
return json.load(json_file)
except FileNotFoundError:
update_database.get_sivas_dbase(self.filename, self.file_dir)
return self.load_from_json()
def load_from_csv(self):
"""Reads partnumbers from a csv-file"""
config_path = os.path.join(self.file_dir, self.filename)
try:
df = pd.read_csv(config_path, sep=';', converters={"Teilenummern":str})
return tuple(df["Teilenummern"])
except FileNotFoundError:
update_database.get_rd_dbase(self.file_dir)
return self.load_from_csv()
class SivasPartnumbers(Partnumbers):
def __init__(self, main_sivas_id: str, file_dir: str):
self.file_dir = file_dir
self.main_sivas_id = main_sivas_id
self._partnumbers = None
super().__init__(file_dir=self.file_dir, filename=self.main_sivas_id)
def lazy_partnumber_init(function):
"""Decorator prevents time-consuming initialization of sivas partnumbers if method doesn't require them"""
def wrapper(self):
if self._partnumbers is None:
self._partnumbers = self.__class__.mro()[1].load_from_json(self) # Using super() isn't possible here
return function(self)
return wrapper
@lazy_partnumber_init
def get_partnumbers(self) -> set:
"""Returns all partnumbers (id's) of the provided json-file"""
ids = set()
for child_id, top_level_assemblies_of_child in self._partnumbers.items():
ids.add(child_id)
for top_level_assembly in top_level_assemblies_of_child:
top_level_assembly_id = top_level_assembly["Parent"]
ids.add(top_level_assembly_id)
return ids
def get_longest_partnumber(self) -> str:
"""Returns the longest partnumber in the provided json-file"""
return max(self.get_partnumbers())
@lazy_partnumber_init
def get_ordered_parent_children(self, no_duplicates: bool = False) -> dict:
"""Replicates the partnumber structure of the given BOM (bill of material) from sivas. The Order of the partnumbers is identical to the position numbers in the BOM"""
unordered_parent_children = {}
for parents in self._partnumbers:
for children in self._partnumbers[parents]:
if children["Parent"] not in unordered_parent_children:
unordered_parent_children[children["Parent"]] = {}
pos = children["Position"].lstrip(".").split("/")[0]
pos_in_list = int(pos)
unordered_parent_children[children["Parent"]][pos_in_list] = parents
# Second dict necessary, because children aren't sorted according to their position numbers in the SIVAS-BOM
ordered_parent_children = {}
for key, values in unordered_parent_children.items():
for subkey, subvalue in sorted(values.items()):
if key not in ordered_parent_children:
ordered_parent_children[key] = []
ordered_parent_children[key].append(subvalue)
return ordered_parent_children
@lazy_partnumber_init
def get_max_bom_depth(self) -> int:
"""Returns the maximum nesting-depth (level) of the provided assembly partnumber as an integer"""
bom_levels = []
for values in self._partnumbers.values():
for value in values:
current_bom_level = value["Position"].count(".")
bom_levels.append(current_bom_level)
return max(bom_levels)
@lazy_partnumber_init
def get_partnumber_descriptions(self) -> dict:
descriptions = {}
for parent, values in self._partnumbers.items():
for value in values:
descriptions[parent] = value["Bezeichnung"]
return descriptions
class RuleDesignerPartnumbers(Partnumbers):
def __init__(self, ruledesigner_ids_dir: str, ruledesigner_ids_filename: str):
self.ruledesigner_ids_dir = ruledesigner_ids_dir
self.ruledesigner_ids_filename = ruledesigner_ids_filename
super().__init__(file_dir=self.ruledesigner_ids_dir, filename=self.ruledesigner_ids_filename)
def get_partnumbers(self):
return super().load_from_csv()
def update_database(self):
return update_database.get_rd_dbase(self.ruledesigner_ids_dir)
class SivasRuleDesignerComparison:
def __init__(self, sivas_ids_dir, sivas_main_id, rd_ids_dir, rd_ids_filename):
self.sivas = SivasPartnumbers(main_sivas_id=sivas_main_id, file_dir=sivas_ids_dir)
self.ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_ids_dir, ruledesigner_ids_filename=rd_ids_filename)
def get_migration_status(self) -> dict:
"""Checks if given sivas-partnumbers have been migrated been migrated into RuleDesigner"""
sivas_ids = self.sivas.get_partnumbers()
ruledesigner_ids = self.ruledesigner.get_partnumbers()
migration_completed = {}
for sivas_partnumber in sivas_ids:
migration_completed[sivas_partnumber] = True if sivas_partnumber in ruledesigner_ids else False
return migration_completed
class ExcelExport: class ExcelExport:
def __init__(self, main_id, sivas_ids_file_dir, rd_ids_file_dir, rd_ids_filename): def __init__(self,
self.main_id = main_id asm_id,
self.environment = Environment() asm_json_dir,
self._sivas = SivasPartnumbers(main_sivas_id=main_id, file_dir=sivas_ids_file_dir) asm_json_filename,
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_ids_file_dir, ruledesigner_ids_filename=rd_ids_filename) asm_csv_dir,
self._comparison = SivasRuleDesignerComparison(sivas_ids_dir=sivas_ids_file_dir, sivas_main_id=main_id, rd_ids_dir=rd_ids_file_dir, rd_ids_filename=rd_ids_filename) asm_csv_filename,
self._parent_children = self._sivas.get_ordered_parent_children() rd_dbase_dir,
self._ruledesigner_ids = self._ruledesigner.get_partnumbers() rd_dbase_filename,
load_local_asm_json,
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
@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): def write_excel(self):
data = self._create_excel_data() data = self._create_excel_data()
output_dir = environment.out_dir out_dir = os.environ.get("CREMIG_WORK")
out_path = os.path.join(out_dir, self.asm_id + ".xlsm")
destiny_filepath = os.path.join(output_dir, self.main_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)
workbook = openpyxl.load_workbook(self.environment.vba_macro_filepath, keep_vba=True)
worksheet = workbook.active worksheet = workbook.active
# Headers # Spaltenüberschriften
for header, column in zip(data, worksheet.iter_cols(min_row=1, max_col=len(data), max_row=1)): for header, column in zip(data, worksheet.iter_cols(min_row=1, max_col=len(data), max_row=1)):
for cell in column: for cell in column:
cell.value = header cell.value = header
# Values below Headers # 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 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): for cell, element in zip(column, value):
cell.value = element cell.value = element
# Renaming the sheet is necessary, because it triggers the execution of the VBA-Macro # Umbenennung des Blattes erforderlich, da die Umbenennung die Ausführung des VBA-Makros triggert.
# The renaming must be performed AFTER inserting the data into the spreadsheet, because the conditional formatting, which is part of the macro, can't be performed # Die Umbenennung muss NACH dem Einfügen der Daten in das Blatt erfolgen, da die bedingte Formatierung,
# when no data is in the spreadsheet # die ebenfalls Bestandteil des Makros ist, nicht ausgeführt werden kann, solange sich keine Daten im Blatt befinden
worksheet.title = "Ergebnis" worksheet.title = "Ergebnis"
workbook.save(destiny_filepath) workbook.save(out_path)
print(f"Excel-Datei erfolgreich in '{output_dir}' geschrieben...") print(f"Excel-Datei erfolgreich in '{out_dir}' geschrieben...")
def _create_excel_data(self) -> dict: def _create_excel_data(self) -> dict:
# Columns in excel-spreadsheet: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung # Spalten im Arbeitsblatt: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung
# Sortierung: Minus-separated hierarchy of the partnumber in the BOM structure # Sortierung: Minus-separierte Hierarchie der Teilenummern in der Stücklistenstruktur
# Teilenummer: SIVAS-partnumber # Teilenummer: SIVAS-Teilenummer
# Bezeichnung: SIVAS-description of the partnumber # Bezeichnung: SIVAS-Bezeichnung der Teilenummer
# Migrationsstatus: "IMPORTIEREN" = partnumber hasn't been migrated yet, "VERFÜGBAR" = partnumber has been migrated # Migrationsstatus: "IMPORTIEREN" = Migration ausstehend, "VERFÜGBAR" = Migration abgeschlossen
# All the other columns remain empty, because their values are entered in the excel spreadsheet # Alle weiteren Spalten bleiben leer, da sie händisch vom User zu befüllen sind
sorting = self._create_sorting_string() sorting = self._create_sorting_string()
migrated = self._comparison.get_migration_status() migrated_ids = self.migration_status.ids_in_rd
descriptions = self._sivas.get_partnumber_descriptions() 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 = [] prt_num = []
description = [] description = []
migration_status = [] migration_status = []
evaluation = [] assessment = []
comment = [] comment = []
for partnumber_string in sorting: for id_str in sorting:
partnumber = partnumber_string.split("-")[-1] id = id_str.split("-")[-1]
prt_num.append(partnumber) prt_num.append(id)
if partnumber not in descriptions: assessment.append('')
comment.append('')
if id not in descriptions:
description.append("") description.append("")
else: else:
description.append(descriptions[partnumber]) description.append(descriptions[id])
migration_status.append("VERFÜGBAR" if migrated[partnumber] is True else "IMPORTIEREN")
evaluation.append('') if id in migrated_ids:
comment.append('') 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 = { data = {
"Sortierung": sorting, "Sortierung": sorting,
"Teilenummer": prt_num, "Teilenummer": prt_num,
"Bezeichnung": description, "Bezeichnung": description,
"Migrationsstatus": migration_status, "Migrationsstatus": migration_status,
"Beurteilung": evaluation, "Beurteilung": assessment,
"Bemerkung": comment "Bemerkung": comment
} }
return data return data
@@ -284,22 +131,22 @@ class ExcelExport:
""" """
if parent is None: if parent is None:
parent = self.main_id parent = self.asm_id
if sorting_strings is None: if sorting_strings is None:
sorting_strings = [parent] sorting_strings = [parent]
children = self._parent_children[parent] parent_children = self.asm_data.parent_children
children = parent_children[parent]
for child in children: for child in children:
sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-")) sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-"))
if child in self._parent_children: if child in parent_children:
self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings) self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings)
return sorting_strings return sorting_strings
class Treeview: class Treeview:
# For correct display of ANSI escape sequences # For correct display of ANSI escape sequences
@@ -315,20 +162,55 @@ class Treeview:
console_colors = {"RED": "\033[41m", console_colors = {"RED": "\033[41m",
"GREEN": "\033[42m", "GREEN": "\033[42m",
"BLUE": "\033[44m",
"RESET": "\033[0m" "RESET": "\033[0m"
} }
def __init__(self, main_partnumber, sivas_src_file, rd_src_dir, rd_src_file): def __init__(self,
self.main_partnumber = main_partnumber asm_id,
self._sivas = SivasPartnumbers(main_sivas_id=main_partnumber, file_dir=sivas_src_file) asm_json_dir,
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_src_dir, ruledesigner_ids_filename=rd_src_file) asm_json_filename,
self._sivas_rd_comparison = SivasRuleDesignerComparison(sivas_main_id=main_partnumber, sivas_ids_dir=sivas_src_file, rd_ids_dir=rd_src_dir, rd_ids_filename=rd_src_file) asm_csv_dir,
self._parent_children = self._sivas.get_ordered_parent_children() asm_csv_filename,
self._descriptions = self._sivas.get_partnumber_descriptions() rd_dbase_dir,
self._max_tree_depth = self._sivas.get_max_bom_depth() rd_dbase_filename,
self._longest_partnumber = self._sivas.get_longest_partnumber() load_local_asm_json,
self._migration_status = self._sivas_rd_comparison.get_migration_status() load_local_asm_csv,
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
@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):
if self._longest_entry is None:
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): def generate_tree(self):
self._tree_header() self._tree_header()
@@ -339,17 +221,23 @@ class Treeview:
"""Prints root number (number for which the user wants to see the tree) to the console""" """Prints root number (number for which the user wants to see the tree) to the console"""
background_color = self._set_text_background_color(self.main_partnumber) id = self.asm_id
print(f"{background_color}{self.main_partnumber}{Treeview.console_colors['RESET']}") 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=""): def _tree_body(self, parent_id=None, prefix=""):
"""Prints the body (all partnumbers except root/main-partnumber) of the tree to the console""" """Prints the body (all partnumbers except root/main-partnumber) of the tree to the console"""
if parent_id is None: if parent_id is None:
parent_id = self.main_partnumber parent_id = self.asm_id
children_ids = self._parent_children[parent_id] parent_children = self.asm_bom_data.parent_children
children_ids = parent_children[parent_id]
for index, child_id in enumerate(children_ids): for index, child_id in enumerate(children_ids):
@@ -360,99 +248,156 @@ class Treeview:
self._print_entry(child_id, prefix, connector_symbol, partnumber_background_color) self._print_entry(child_id, prefix, connector_symbol, partnumber_background_color)
if child_id in self._parent_children: if child_id in parent_children:
next_prefix = self._prefix(prefix, is_last_assembly_child) next_prefix = self._prefix(prefix, is_last_assembly_child)
self._tree_body(parent_id=child_id, prefix=next_prefix) self._tree_body(parent_id=child_id, prefix=next_prefix)
def _tree_summary(self): def _tree_summary(self):
amount_all_sivas_ids = len(self._migration_status) amount_in_rd = len(self.migration_status.ids_in_rd)
amount_migrated_ids = sum(migration_status == True for migration_status in self._migration_status.values()) amount_pending = len(self.migration_status.ids_pending)
amount_unmigrated_ids = amount_all_sivas_ids - amount_migrated_ids
progress = int(amount_migrated_ids / amount_all_sivas_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 column_width = 5
border_padding = "".ljust(column_width, "") border_padding = "".ljust(column_width, "")
empty_padding = "".ljust(column_width, " ") empty_padding = "".ljust(column_width, " ")
print(f"""
╭─────────────────────────────────────{border_padding} red_entry = f"{Treeview.console_colors['RED'] } {Treeview.console_colors['RESET']}│ Migration ausstehend │ {str(amount_pending).ljust(column_width)}"
│ Auswertung {empty_padding}
├───────────────────────────────────┬─{border_padding} if not self.no_blue_ids:
│ Anzahl abgeschlossene Migrationen{str(amount_migrated_ids).ljust(column_width)} blue_entry = f"{Treeview.console_colors['BLUE'] } {Treeview.console_colors['RESET']}│ PDM-Migration abgeschlossen {str(amount_in_work).ljust(column_width)}\n"
│ Anzahl fehlende Migrationen {str(amount_unmigrated_ids).ljust(column_width)} green_entry = f"{Treeview.console_colors['GREEN']} {Treeview.console_colors['RESET']}│ Migration abgeschlossen (Freigegeben){str(amount_released).ljust(column_width)}\n"
├───────────────────────────────────┼─{border_padding}
│ Migrationsfortschritt │ {f"{progress}%".ljust(column_width)} else:
╰───────────────────────────────────┴─{border_padding} 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): def _print_entry(self, child_id, prefix, connector, color):
"""Prints tree entry""" """Eintrag in die Konsole drucken"""
# 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) text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(self.longest_entry)
description = self._descriptions[child_id] 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): 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 return True if child_index == len(children) - 1 else False
def _set_connector_symbol(self, child_is_last_child): 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"] return Treeview.connector_symbols["ELBOW"] if child_is_last_child else Treeview.connector_symbols["T"]
def _prefix(self, prefix, child_is_last_child): 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"] prefix += Treeview.prefix_types["EMPTY"] if child_is_last_child else Treeview.prefix_types["PIPE"]
return prefix 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 """Legt Hintergrundfarbe der Teilenummer basierend auf ihrem Migrationsstatus fest:
- Migration completed: green
- Migration pending: red"""
return Treeview.console_colors["GREEN"] if self._migration_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__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Das Programm gibt Auskunft über den aktuellen Migrationsstand', prog='compare_lists') 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('-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('-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='Erzeugt Excel-Datei in der alle Teilenummern, die migriert werden müssen, aufgelistet sind') 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')
args = parser.parse_args() args = parser.parse_args()
environment = Environment() asm_id = args.sivasid.lstrip()
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: if args.excel:
num = args.sivasid.lstrip() excel_comparison = ExcelExport(asm_id=asm_id,
excel_comparison = ExcelExport(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename) 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)
excel_comparison.write_excel() excel_comparison.write_excel()
elif args.tree: elif args.tree:
num = args.sivasid.lstrip() treeview_comparison = Treeview(asm_id=asm_id,
treeview_comparison = Treeview(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename) 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() treeview_comparison.generate_tree()
else: else:
parser.print_help() parser.print_help()
+19 -30
View File
@@ -2,8 +2,7 @@ import os
from tkinter import messagebox from tkinter import messagebox
import update_database import update_database
from manage_configs import SivasAssemblyData from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus
from get_teilestamm import FetchSivasTeilestamm
""" """
Contains the logic for the MVC of CreoMigrate Gui Contains the logic for the MVC of CreoMigrate Gui
@@ -96,6 +95,8 @@ class Controller(Observer):
ret = self._environment.cfg_dir ret = self._environment.cfg_dir
elif env_path == "log": elif env_path == "log":
ret = self._environment.log_dir ret = self._environment.log_dir
elif env_path == "rd_dbase_filename":
ret = self._environment.ruledesigner_csv_filename
else: else:
raise Exception("this path is not defined "+env_path) raise Exception("this path is not defined "+env_path)
return ret return ret
@@ -114,36 +115,26 @@ class Controller(Observer):
orange[red] = True orange[red] = True
return orange return orange
def get_csv_files_of_asm(self, asmid, uselocal): def get_csv_files_of_asm(self, asm_id, load_local_asm_json, load_local_rd_dbase):
""" sich die Einzelteile als .csv und den Aufbau des Assemblies als .json holen """ """ Holt sich die Metadaten der Stücklistenpositionen als CSV"""
data_dir = self.get_env("data") data_dir = self.get_env("data")
sivas_teilestamm = FetchSivasTeilestamm(asmid, data_dir) rd_dbase_filename = self.get_env("rd_dbase_filename")
# falls uselocal True, dann keine Serverabfrage, sondern mit den lokalen Daten arbeiten asm_data = SivasAssemblyData(data_dir, asm_id + '.json', load_local_asm_json)
sivas_teilestamm.fetch_from_db(uselocal) rd_data = RuleDesignerDatabaseConfig(data_dir, rd_dbase_filename, load_local_rd_dbase)
if not uselocal: migration_status = MigrationStatus(asm_data.child_ids, rd_data.ids)
if not load_local_asm_json:
try: try:
sivas_teilestamm.get_unmigrated_teilstamm_csvs() update_database.get_sivas_teilestamm(migration_status.ids_pending, data_dir)
except: except:
self.message_error('Abfrage hat nicht funktioniert', 'sivas_db_error') self.message_error('Abfrage hat nicht funktioniert', 'sivas_db_error')
return return
redids = sivas_teilestamm.get_red_ids() self._model.set_red_ids(migration_status.ids_pending)
self._model.set_parent_children(asm_data.parent_children)
sad = SivasAssemblyData(data_dir, asmid + '.json') self._model.set_asm_id(asm_id)
parent_children = sad.get_parent_children() self._model.set_bom_properties(asm_data.all_bom_properties)
self._model.set_red_ids(redids)
self._model.set_parent_children(parent_children)
self._model.set_asm_id(asmid)
def get_bom_properties(self, asmid):
""" Holt sich aus Stückliste alle Eigenschaften der Stücklistenpositionen (z.B. Bezeichnung)"""
data_dir = self.get_env("data")
assembly_data = SivasAssemblyData(data_dir, asmid + '.json')
bom_properties = assembly_data.get_all_bom_properties()
self._model.set_bom_properties(bom_properties)
def set_model(self, model): def set_model(self, model):
self._model = model self._model = model
@@ -162,15 +153,13 @@ class Controller(Observer):
def get_current_asm_id(self): def get_current_asm_id(self):
self._model.get_asm_id() self._model.get_asm_id()
def set_current_asm_id(self, asm_id, loadlocal=False): def set_current_asm_id(self, asm_id, load_local_asm_json=False, load_local_rd_dbase=False):
if not asm_id.isnumeric(): if not asm_id.isnumeric():
return return
# TODO save current given data. In case, ask whether you should save or drop # TODO save current given data. In case, ask whether you should save or drop
self._model.set_asm_id(asm_id) self._model.set_asm_id(asm_id)
# hole alle Daten vom Server # hole alle Daten vom Server
self.get_csv_files_of_asm(asm_id, loadlocal) self.get_csv_files_of_asm(asm_id, load_local_asm_json, load_local_rd_dbase)
self.get_bom_properties(asm_id)
#self._load_cid_data2model(asm_id)
def set_current_item_id(self, cid): def set_current_item_id(self, cid):
"""setze eine neue Item ID """ """setze eine neue Item ID """
@@ -301,7 +290,7 @@ class Controller(Observer):
elif key == 'cb_ladelokal_asm': elif key == 'cb_ladelokal_asm':
current_asmid = self.data_from_view('asm_id') current_asmid = self.data_from_view('asm_id')
# hole dir nur die lokalen Daten. Keine Datenbankabfrage # hole dir nur die lokalen Daten. Keine Datenbankabfrage
self.set_current_asm_id(current_asmid, True) self.set_current_asm_id(current_asmid, load_local_asm_json=True, load_local_rd_dbase=True)
elif key == 'cb_3dsuchen': elif key == 'cb_3dsuchen':
self.suche_3d() self.suche_3d()
-50
View File
@@ -1,50 +0,0 @@
import argparse
import os
from manage_configs import MigrationStatus
import update_database
class FetchSivasTeilestamm:
def __init__(self, assembly_json_name, assembly_json_out_dir=os.environ.get('CREMIG_DATA'), csv_out_dir=os.environ.get('CREMIG_DATA')):
self.assembly_json_out_dir = assembly_json_out_dir
self.assembly_json_name = str(assembly_json_name).replace('.json', "") + ".json"
self.csv_out_dir = csv_out_dir
self.migration = None
def fetch_from_db(self, use_local=False):
self.migration = MigrationStatus(self.assembly_json_out_dir, self.assembly_json_name)
if use_local:
self.migration.use_local()
def get_unmigrated_teilstamm_csvs(self):
"""Generiert CSV-Dateien für alle noch nicht migrierten Stücklistenteile der angegebenen Baugruppennummer"""
if self.migration == None:
raise Exception("not yet connected to local data or database")
unmigrated_ids = self.migration.get_unmigrated_ids()
update_database.get_sivas_teilestamm(unmigrated_ids, self.csv_out_dir)
def get_red_ids(self):
if self.migration == None:
raise Exception("not yet connected to local data or database")
return self.migration.get_unmigrated_ids()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='fetches data from sivas or ruledesigner fusion', prog='update_database')
parser.add_argument('-s', '--sivasid', action='store', help='fetch teilestamm-data for all unmigrated parts in the bom of a given asm-id. Specifiy multiple ids as comma separated values without space, e.g. 200000089,200000090', metavar='id(s)')
parser.add_argument('-l', '--uselocal', action='store_true', help='Do not connect to database. Use local files (if already existing) for the given asm-id', default=False)
args = parser.parse_args()
out_dir = os.environ.get('CREMIG_DATA')
if args.sivasid:
sivas_asm_id = args.sivasid.strip().replace(".json","")
sivas_teilestamm = FetchSivasTeilestamm(sivas_asm_id, out_dir)
if args.uselocal:
sivas_teilestamm.use_local(args.uselocal)
sivas_teilestamm.fetch_from_db()
if args.uselocal == False:
sivas_teilestamm.get_unmigrated_teilstamm_csvs(args.uselocal)
else:
parser.print_help()
+252 -108
View File
@@ -1,6 +1,7 @@
import os import os
import json import json
from configparser import ConfigParser, RawConfigParser import pymssql
from configparser import ConfigParser
import pandas as pd import pandas as pd
import update_database import update_database
@@ -10,15 +11,20 @@ Diese Lib verwaltet alle Config dateien (als .cfg oder als .json) von CreoMigrat
""" """
class JsonBasedConfig: class JsonBasedConfig:
"""Klasse mit generellen Methoden für alle Config Objekte mit json files""" """Klasse mit generellen Methoden für alle Config Objekte mit json files"""
def __init__(self, config_dir, config_file): def __init__(self, config_dir: str, config_file: str):
"""merke dir den Pfad zu den Configdateien""" """merke dir den Pfad zu den Configdateien"""
self._config_dir = config_dir
self._config_file = config_file
self._config_path = os.path.join(config_dir, config_file) self._config_path = os.path.join(config_dir, config_file)
def load_json_from_file(self): def load_json_from_file(self):
"""lade das json file vom gegeben Pfad in ein json Objekt""" """lade das json file vom gegeben Pfad in ein json Objekt"""
with open(self._config_path, 'r', encoding='utf-8') as fh: try:
content = fh.read() with open(self._config_path, 'r', encoding='utf-8') as fh:
return json.loads(content) 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): def from_json(self, json_str):
"""nimm den Json String zur Initialisierung des Objektes""" """nimm den Json String zur Initialisierung des Objektes"""
@@ -27,8 +33,10 @@ class JsonBasedConfig:
class CSVBasedConfig: class CSVBasedConfig:
def __init__(self, config_dir, config_file): def __init__(self, config_dir: str, config_file: str):
"""merke dir den Pfad zu den Configdateien""" """merke dir den Pfad zu den Configdateien"""
self._config_dir = config_dir
self._config_file = config_file
self._config_path = os.path.join(config_dir, config_file) self._config_path = os.path.join(config_dir, config_file)
def load_csv_from_file(self): def load_csv_from_file(self):
@@ -36,156 +44,294 @@ class CSVBasedConfig:
return pd.read_csv(self._config_path, sep=";", dtype="object") 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): class SivasAssemblyData(JsonBasedConfig):
"""Auslesen der Assembly-Informationen zu einer angegebenen Baugruppennummer""" """Auslesen der Assembly-Informationen zu einer angegebenen Baugruppennummer"""
def __init__(self, config_dir, config_file): def __init__(self, config_dir: str, config_file: str, load_local_json=False):
super().__init__(config_dir, config_file) super().__init__(config_dir, config_file)
self.assembly_data = None self._load_local_json = load_local_json
self._asm_data = None
self._child_ids = None
self._parent_children = None
self._longest_child_id = None
self._max_bom_depth = None
self._property_names = None
self._all_bom_properties = None
def _lazy_init_assembly_data(func): def create_asm_json(self):
"""Sorgt dafür, dass json-Datei der Baugruppe nur 1x geladen wird""" """Erstellt JSON mit den aktuellen Baugruppeninformationen"""
try:
update_database.get_sivas_dbase(self._config_file.removesuffix(".json"))
except:
raise ValueError("JSON-Datei mit Baugruppeninformationen konnte nicht erstellt werden!")
def wrapper(self, *args, **kwargs): @property
if self.assembly_data is None: def asm_data(self) -> dict:
self.assembly_data = self.load_json_from_file() """Liest die Baugruppen-JSON ein und gibt deren Informationen zurück"""
return func(self, *args, **kwargs) if self._asm_data is None:
return wrapper
@_lazy_init_assembly_data if not self._load_local_json:
def get_assembly_ids(self): self.create_asm_json()
self._asm_data = self.load_json_from_file()
return self._asm_data
@property
def child_ids(self) -> set:
"""Gibt alle Teilenummern der Stückliste zurück (auch Teilenummern aus tieferen Baugruppenebenen)""" """Gibt alle Teilenummern der Stückliste zurück (auch Teilenummern aus tieferen Baugruppenebenen)"""
assembly_ids = set() if self._child_ids is None:
for child_id, parent_properties in self.assembly_data.items(): self._child_ids = set()
assembly_ids.add(child_id) for child_id, parent_properties in self.asm_data.items():
for parent_property in parent_properties: self._child_ids.add(child_id)
parent_id = parent_property["Parent"] for parent_property in parent_properties:
assembly_ids.add(parent_id) parent_id = parent_property["Parent"]
return assembly_ids self._child_ids.add(parent_id)
return self._child_ids
@_lazy_init_assembly_data @property
def get_parent_children(self) -> dict: def parent_children(self) -> dict:
"""Ordnet Stücklistenpositionen (Kinder) den Baugruppennummern (Eltern) zu. Die Kinder sind entsprechend ihrer Positionsnummern in den Stücklisten sortiert.""" """Ordnet Stücklistenpositionen (Kinder) den Baugruppennummern (Eltern) zu. Die Kinder sind entsprechend ihrer Positionsnummern in den Stücklisten sortiert."""
unordered_parent_children = {} if self._parent_children is None:
unordered_parent_children = {}
for child_id, parent_properties in self.assembly_data.items(): for child_id, parent_properties in self.asm_data.items():
for parent_property in parent_properties: for parent_property in parent_properties:
parent_id = parent_property["Parent"] parent_id = parent_property["Parent"]
child_posnr_in_parent_bom = int(parent_property["Position"].lstrip(".").split("/")[0]) child_posnr_in_parent_bom = int(parent_property["Position"].lstrip(".").split("/")[0])
if parent_id not in unordered_parent_children: if parent_id not in unordered_parent_children:
unordered_parent_children[parent_id] = {} unordered_parent_children[parent_id] = {}
unordered_parent_children[parent_id][child_posnr_in_parent_bom] = child_id unordered_parent_children[parent_id][child_posnr_in_parent_bom] = child_id
ordered_parent_children = {} ordered_parent_children = {}
for parent, children in unordered_parent_children.items(): for parent, children in unordered_parent_children.items():
for pos_nr, child in sorted(children.items()): for pos_nr, child in sorted(children.items()):
if parent not in ordered_parent_children: if parent not in ordered_parent_children:
ordered_parent_children[parent] = [] ordered_parent_children[parent] = []
ordered_parent_children[parent].append(child) ordered_parent_children[parent].append(child)
return ordered_parent_children self._parent_children = ordered_parent_children
return self._parent_children
def get_longest_assembly_id(self) -> str: @property
def longest_child_id(self) -> str:
"""Gibt die längste Teilenummer von allen Stücklistenpositionen zurück""" """Gibt die längste Teilenummer von allen Stücklistenpositionen zurück"""
return max(self.get_assembly_ids()) if self._longest_child_id is None:
self._longest_child_id = max(self.child_ids)
return max(self.child_ids)
@_lazy_init_assembly_data @property
def get_max_bom_depth(self) -> int: def max_bom_depth(self) -> int:
"""Gibt die tiefste Stücklistenebene zurück (0 = höchste Ebene)""" """Gibt die tiefste Stücklistenebene zurück (0 = höchste Ebene)"""
bom_levels = [] if self._max_bom_depth is None:
for parents in self.assembly_data.values(): bom_levels = []
for parent in parents: for parents in self.asm_data.values():
current_bom_level = parent["Position"].count(".") for parent in parents:
bom_levels.append(current_bom_level) current_bom_level = parent["Position"].count(".")
return max(bom_levels) bom_levels.append(current_bom_level)
self._max_bom_depth = max(bom_levels)
return self._max_bom_depth
@_lazy_init_assembly_data @property
def get_property_names(self) -> list: def property_names(self) -> list:
"""Gibt alle Eigenschaftsnamen zurück, die in der Assembly-json-Datei enthalten sind""" """Gibt alle Eigenschaftsnamen zurück, die in der Assembly-json-Datei enthalten sind"""
for parent_data in self.assembly_data.values(): if self._property_names is None:
return list(parent_data[0]) for parent_data in self.asm_data.values():
self._property_names = list(parent_data[0])
return self._property_names
@_lazy_init_assembly_data
def get_bom_property_values(self, property) -> dict: def get_bom_property_values(self, property) -> dict:
"""Gibt eine Eigenschaft zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)""" """Gibt eine Eigenschaft zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
if property not in self.get_property_names(): if property not in self.property_names:
raise ValueError("Angegebener Eigenschaftsname exisitert nicht!") raise ValueError("Angegebener Eigenschaftsname exisitert nicht!")
id_property_mapping = {} id_property_mapping = {}
for child, parent_data in self.assembly_data.items(): for child, parent_data in self.asm_data.items():
for properties in parent_data: for properties in parent_data:
if property in properties: if property in properties:
id_property_mapping[child] = properties[property] id_property_mapping[child] = properties[property]
return id_property_mapping return id_property_mapping
@_lazy_init_assembly_data @property
def get_all_bom_properties(self) -> dict: def all_bom_properties(self) -> dict:
"""Gibt alle Eigenschaften zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)""" """Gibt alle Eigenschaften zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
all_properties = {} if self._all_bom_properties is None:
property_names = self.get_property_names() self._all_bom_properties = {}
for property_name in property_names: for property_name in self.property_names:
all_properties[property_name] = self.get_bom_property_values(property_name) self._all_bom_properties[property_name] = self.get_bom_property_values(property_name)
return all_properties return self._all_bom_properties
class RuleDesignerDatabaseConfig(CSVBasedConfig): class RuleDesignerDatabaseConfig(CSVBasedConfig):
"""Auslesen der Angaben aus der RuleDesigner-Datenbank-CSV""" """Auslesen der CSV-Datei, in der alle RuleDesigner-Teilenummern aufgelistet sind"""
def __init__(self, config_dir=".", config_file="RD_Teilenummern.csv"): def __init__(self, config_dir=".", config_file="RD_Teilenummern.csv", load_local_csv=False):
super().__init__(config_dir, config_file) super().__init__(config_dir, config_file)
self._load_local_csv = load_local_csv
self._ids = None
def get_ruledesigner_ids(self): def copy_csv_from_server(self):
return tuple(self.load_csv_from_file()['Teilenummern']) """Kopiert CSV-Datei vom Server, in der alle Teilenummern, die sich in RuleDesigner befinden"""
try:
update_database.get_rd_dbase()
except:
raise ValueError("CSV mit RuleDesigner-Teilenummern konnte nicht ins lokale Verzeichnis kopiert werden!")
@property
def ids(self):
if self._ids is None:
if not self._load_local_csv:
self.copy_csv_from_server()
self._ids = tuple(self.load_csv_from_file()['Teilenummern'])
return self._ids
class MigrationStatus: class MigrationStatus:
def __init__(self, file_dir, assembly_json): """Klasse gibt Informationen zum aktuellen Migrationsstand zurück, indem geprüft wird, ob die Nummern aus Sivas, auch in RuleDesigner vorliegen"""
self._file_dir = file_dir def __init__(self, asm_child_ids: tuple, rd_ids: tuple):
self._assembly_json = assembly_json self.asm_child_ids = asm_child_ids
self._uselocal = False self.rd_ids = rd_ids
self._rd_statuses = None
self._overview = None
self._ids_in_rd = None
self._ids_in_progress = None
self._ids_pending = None
def use_local(self): @property
self._uselocal = True def id_status(self) -> dict:
"""Gibt zurück, ob die Stücklistenpositionen der Baugruppe migriert sind, oder nicht"""
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
def get_migration_status(self) -> dict: @property
"""Gibt zurück, ob die Teilenummern der Baugruppe bereits migriert wurden, oder nicht""" def ids_in_rd(self) -> list:
out_dir = self._file_dir """Gibt alle Stücklistenpositionen zurück, die migriert sind"""
if self._uselocal == False:
ok = update_database.get_sivas_dbase(self._assembly_json, out_dir)
if not ok:
raise Exception("no json file could be generated")
update_database.get_rd_dbase()
else:
if not os.path.exists(os.path.join(out_dir, self._assembly_json)):
raise Exception("no local file exists "+ self._assembly_json)
self.sivas_assembly_data = SivasAssemblyData(out_dir, self._assembly_json)
sivas_assembly_ids = self.sivas_assembly_data.get_assembly_ids()
self.ruledesigner_data = RuleDesignerDatabaseConfig(out_dir)
ruledesigner_ids = self.ruledesigner_data.get_ruledesigner_ids()
migration_status = {} if self._ids_in_rd is None:
for sivas_assembly_id in sivas_assembly_ids: self._ids_in_rd = [id for id, status in self.id_status.items() if status is True]
migration_status[sivas_assembly_id] = True if sivas_assembly_id in ruledesigner_ids else False return self._ids_in_rd
return migration_status
def get_migrated_ids(self) -> list: @property
"""Gibt alle Teilenummern aus der Baugruppenstückliste zurück, die bereits in RuleDesigner migriert wurden""" 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"""
migrated_ids = [assembly_id for assembly_id, migration_status in self.get_migration_status().items() if migration_status is True]
return migrated_ids
def get_unmigrated_ids(self) -> list: if self._ids_in_progress is None:
"""Gibt alle Teilenummern aus der Baugruppenstückliste zurück, deren Migration in RuleDesigner noch aussteht"""
unmigrated_ids = [assembly_id for assembly_id, migration_status in self.get_migration_status().items() if migration_status is False] rd_sql_data = RuleDesignerSQLData()
return unmigrated_ids 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 ids_pending(self) -> list:
"""Gibt alle Stücklistenpositionen zurück, die noch nicht migriert sind"""
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):
def __init__(self, config_dir, config_file, load_local_csv=False):
super().__init__(config_dir, config_file)
self._load_local_csv = load_local_csv
self._csv_data = None
@staticmethod
def create_csvs(ids):
"""Erstellt CSV-Datei(en) mit den Teilestamminformationen zu der / den angegebenen Teilenummer(n)"""
try:
update_database.get_sivas_teilestamm(ids)
except:
raise ValueError("CSV(s) konnte nicht generiert werden!")
@property
def csv_data(self):
"""Gibt CSV-Daten als Dataframe zurück"""
if self._csv_data is None:
if not self._load_local_csv:
self.create_csvs(self._config_file.removesuffix(".csv"))
self._csv_data = self.load_csv_from_file()
return self._csv_data
def get_property_values(self, property_name) -> str:
"""Gibt Wert aus der CSV für den angegebenen Spaltennamen zurück"""
ret = self.csv_data[property_name][0]
return ret
class TOSGroupsConfig(JsonBasedConfig): class TOSGroupsConfig(JsonBasedConfig):
@@ -525,7 +671,7 @@ class Configs:
self.TOSGruppen = TOSGroupsConfig(cfg_dir) self.TOSGruppen = TOSGroupsConfig(cfg_dir)
self.Materialien = MaterialTransConfig(cfg_dir) self.Materialien = MaterialTransConfig(cfg_dir)
self._TeileNummern = None self._teilenummern = None
self._blacklist = None self._blacklist = None
def get_cfg_path(self): def get_cfg_path(self):
@@ -539,9 +685,9 @@ class Configs:
def get_ruledesigner_ids(self): def get_ruledesigner_ids(self):
if not self._data_dir: if not self._data_dir:
raise Exception("set_data_dir not called. path not set") raise Exception("set_data_dir not called. path not set")
if not self._TeileNummern: if not self._teilenummern:
self._TeileNummern = RuleDesignerDatabaseConfig(self._data_dir).get_ruledesigner_ids() self._teilenummern = RuleDesignerDatabaseConfig(self._data_dir, load_local_csv=True).ids
return self._TeileNummern return self._teilenummern
def get_blacklist(self): def get_blacklist(self):
if not self._data_dir: if not self._data_dir:
@@ -551,5 +697,3 @@ class Configs:
lines = f.read().splitlines() lines = f.read().splitlines()
self._blacklist = lines self._blacklist = lines
return self._blacklist return self._blacklist
+2 -2
View File
@@ -63,7 +63,7 @@ def get_rd_dbase(out_dir=os.environ.get('CREMIG_DATA')):
print(f"File with ruldesigner-partnumbers ${csv_source_path} doesn't exist. Check VPN connection and %RD_DATABASE_SOURCE% + %RD_DATABASE_NAME% in cremig_setenv.bat") print(f"File with ruldesigner-partnumbers ${csv_source_path} doesn't exist. Check VPN connection and %RD_DATABASE_SOURCE% + %RD_DATABASE_NAME% in cremig_setenv.bat")
sys.exit(0) sys.exit(0)
def get_sivas_dbase(sivas_id, out_dir): def get_sivas_dbase(sivas_id, out_dir=os.environ.get('CREMIG_DATA')):
sivas_exe_path = os.environ.get('SIVAS_DATABASE_QUERY') sivas_exe_path = os.environ.get('SIVAS_DATABASE_QUERY')
sivas_id = sivas_id.replace(".json", "") sivas_id = sivas_id.replace(".json", "")
@@ -86,7 +86,7 @@ def get_sivas_dbase(sivas_id, out_dir):
print(f"Can't execute SIVAS-database query. Check VPN connection and check the path of %SIVAS_DATABASE_QUERY% in cremig_setenv.bat") print(f"Can't execute SIVAS-database query. Check VPN connection and check the path of %SIVAS_DATABASE_QUERY% in cremig_setenv.bat")
return False return False
def get_sivas_teilestamm(sivas_ids, out_dir, overwrite=True): def get_sivas_teilestamm(sivas_ids, out_dir=os.environ.get('CREMIG_DATA'), overwrite=True):
"""call the sivas exe for getting all csv files with the item data for the given sivas ids""" """call the sivas exe for getting all csv files with the item data for the given sivas ids"""
sivas_teilestamm_exe = os.environ.get('SIVAS_TEILESTAMM') sivas_teilestamm_exe = os.environ.get('SIVAS_TEILESTAMM')
Binary file not shown.