Compare commits
8 Commits
5bd83324b2
...
9b3c3859cc
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b3c3859cc | |||
| 22b0cf8afc | |||
| e2bfa2aac6 | |||
| 5f3615548b | |||
| 4f740e81ad | |||
| 6f188649ff | |||
| beb1fdc0da | |||
| da4eed34c3 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+227
-282
@@ -1,271 +1,118 @@
|
||||
import update_database
|
||||
|
||||
import warnings
|
||||
#Wegen Pandas Deprecation eingebaut
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus, TeilestammMetadata
|
||||
import argparse
|
||||
import os
|
||||
import json
|
||||
import pandas as pd
|
||||
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 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:
|
||||
|
||||
def __init__(self, main_id, sivas_ids_file_dir, rd_ids_file_dir, rd_ids_filename):
|
||||
self.main_id = main_id
|
||||
self.environment = Environment()
|
||||
self._sivas = SivasPartnumbers(main_sivas_id=main_id, file_dir=sivas_ids_file_dir)
|
||||
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_ids_file_dir, ruledesigner_ids_filename=rd_ids_filename)
|
||||
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)
|
||||
self._parent_children = self._sivas.get_ordered_parent_children()
|
||||
self._ruledesigner_ids = self._ruledesigner.get_partnumbers()
|
||||
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):
|
||||
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):
|
||||
|
||||
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 = openpyxl.load_workbook(self.environment.vba_macro_filepath, keep_vba=True)
|
||||
workbook_path = os.path.join(os.environ.get("CREMIG_LIB"), os.environ.get("VBA_MACRO_NAME"))
|
||||
workbook = openpyxl.load_workbook(workbook_path, keep_vba=True)
|
||||
worksheet = workbook.active
|
||||
|
||||
# Headers
|
||||
# Spaltenüberschriften
|
||||
for header, column in zip(data, worksheet.iter_cols(min_row=1, max_col=len(data), max_row=1)):
|
||||
for cell in column:
|
||||
cell.value = header
|
||||
|
||||
# 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 cell, element in zip(column, value):
|
||||
cell.value = element
|
||||
|
||||
# Renaming the sheet is necessary, because it triggers the execution of the VBA-Macro
|
||||
# The renaming must be performed AFTER inserting the data into the spreadsheet, because the conditional formatting, which is part of the macro, can't be performed
|
||||
# when no data is in the spreadsheet
|
||||
# Umbenennung des Blattes erforderlich, da die Umbenennung die Ausführung des VBA-Makros triggert.
|
||||
# Die Umbenennung muss NACH dem Einfügen der Daten in das Blatt erfolgen, da die bedingte Formatierung,
|
||||
# die ebenfalls Bestandteil des Makros ist, nicht ausgeführt werden kann, solange sich keine Daten im Blatt befinden
|
||||
worksheet.title = "Ergebnis"
|
||||
|
||||
workbook.save(destiny_filepath)
|
||||
print(f"Excel-Datei erfolgreich in '{output_dir}' geschrieben...")
|
||||
workbook.save(out_path)
|
||||
print(f"Excel-Datei erfolgreich in '{out_dir}' geschrieben...")
|
||||
|
||||
def _create_excel_data(self) -> dict:
|
||||
|
||||
# Columns in excel-spreadsheet: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung
|
||||
# Sortierung: Minus-separated hierarchy of the partnumber in the BOM structure
|
||||
# Teilenummer: SIVAS-partnumber
|
||||
# Bezeichnung: SIVAS-description of the partnumber
|
||||
# Migrationsstatus: "IMPORTIEREN" = partnumber hasn't been migrated yet, "VERFÜGBAR" = partnumber has been migrated
|
||||
# All the other columns remain empty, because their values are entered in the excel spreadsheet
|
||||
# Spalten im Arbeitsblatt: Sortierung, Teilenummer, Bezeichnung, Bestandsabfrage, Teilebeurteilung, Zusatzbemerkung
|
||||
# Sortierung: Minus-separierte Hierarchie der Teilenummern in der Stücklistenstruktur
|
||||
# Teilenummer: SIVAS-Teilenummer
|
||||
# Bezeichnung: SIVAS-Bezeichnung der Teilenummer
|
||||
# Migrationsstatus: "IMPORTIEREN" = Migration ausstehend, "VERFÜGBAR" = Migration abgeschlossen
|
||||
# Alle weiteren Spalten bleiben leer, da sie händisch vom User zu befüllen sind
|
||||
|
||||
sorting = self._create_sorting_string()
|
||||
migrated = self._comparison.get_migration_status()
|
||||
descriptions = self._sivas.get_partnumber_descriptions()
|
||||
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 = []
|
||||
migration_status = []
|
||||
evaluation = []
|
||||
assessment = []
|
||||
comment = []
|
||||
|
||||
for partnumber_string in sorting:
|
||||
partnumber = partnumber_string.split("-")[-1]
|
||||
prt_num.append(partnumber)
|
||||
if partnumber not in descriptions:
|
||||
description.append("")
|
||||
else:
|
||||
description.append(descriptions[partnumber])
|
||||
migration_status.append("VERFÜGBAR" if migrated[partnumber] is True else "IMPORTIEREN")
|
||||
evaluation.append('')
|
||||
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])
|
||||
|
||||
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,
|
||||
"Bezeichnung": description,
|
||||
"Migrationsstatus": migration_status,
|
||||
"Beurteilung": evaluation,
|
||||
"Beurteilung": assessment,
|
||||
"Bemerkung": comment
|
||||
}
|
||||
return data
|
||||
@@ -284,21 +131,21 @@ class ExcelExport:
|
||||
"""
|
||||
|
||||
if parent is None:
|
||||
parent = self.main_id
|
||||
parent = self.asm_id
|
||||
if sorting_strings is None:
|
||||
sorting_strings = [parent]
|
||||
|
||||
children = self._parent_children[parent]
|
||||
parent_children = self.asm_data.parent_children
|
||||
children = parent_children[parent]
|
||||
|
||||
for child in children:
|
||||
|
||||
sorting_strings.append(f"{previous_parents}-{parent}-{child}".lstrip("-"))
|
||||
|
||||
if child in self._parent_children:
|
||||
if child in parent_children:
|
||||
self._create_sorting_string(parent=child, previous_parents=previous_parents + f"-{parent}", sorting_strings=sorting_strings)
|
||||
|
||||
return sorting_strings
|
||||
|
||||
|
||||
class Treeview:
|
||||
|
||||
@@ -315,20 +162,55 @@ class Treeview:
|
||||
|
||||
console_colors = {"RED": "\033[41m",
|
||||
"GREEN": "\033[42m",
|
||||
"BLUE": "\033[44m",
|
||||
"RESET": "\033[0m"
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, main_partnumber, sivas_src_file, rd_src_dir, rd_src_file):
|
||||
self.main_partnumber = main_partnumber
|
||||
self._sivas = SivasPartnumbers(main_sivas_id=main_partnumber, file_dir=sivas_src_file)
|
||||
self._ruledesigner = RuleDesignerPartnumbers(ruledesigner_ids_dir=rd_src_dir, ruledesigner_ids_filename=rd_src_file)
|
||||
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)
|
||||
self._parent_children = self._sivas.get_ordered_parent_children()
|
||||
self._descriptions = self._sivas.get_partnumber_descriptions()
|
||||
self._max_tree_depth = self._sivas.get_max_bom_depth()
|
||||
self._longest_partnumber = self._sivas.get_longest_partnumber()
|
||||
self._migration_status = self._sivas_rd_comparison.get_migration_status()
|
||||
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):
|
||||
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):
|
||||
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"""
|
||||
|
||||
background_color = self._set_text_background_color(self.main_partnumber)
|
||||
print(f"{background_color}{self.main_partnumber}{Treeview.console_colors['RESET']}")
|
||||
id = self.asm_id
|
||||
color = self._set_text_background_color(id)
|
||||
|
||||
text_before_description = f"{color}{id}{Treeview.console_colors['RESET']}".ljust(self.longest_entry)
|
||||
description = self.asm_metadata.get_property_values("Bezeichnung1")
|
||||
|
||||
print(f"{text_before_description}{description}")
|
||||
|
||||
def _tree_body(self, parent_id=None, prefix=""):
|
||||
|
||||
"""Prints the body (all partnumbers except root/main-partnumber) of the tree to the console"""
|
||||
|
||||
if parent_id is None:
|
||||
parent_id = self.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):
|
||||
|
||||
@@ -360,99 +248,156 @@ class Treeview:
|
||||
|
||||
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)
|
||||
self._tree_body(parent_id=child_id, prefix=next_prefix)
|
||||
|
||||
def _tree_summary(self):
|
||||
|
||||
amount_all_sivas_ids = len(self._migration_status)
|
||||
amount_migrated_ids = sum(migration_status == True for migration_status in self._migration_status.values())
|
||||
amount_unmigrated_ids = amount_all_sivas_ids - amount_migrated_ids
|
||||
amount_in_rd = len(self.migration_status.ids_in_rd)
|
||||
amount_pending = len(self.migration_status.ids_pending)
|
||||
|
||||
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
|
||||
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"""
|
||||
|
||||
# Necessary for calculating the padding-width after the partnumber
|
||||
longest_entry = self._max_tree_depth * len(Treeview.prefix_types["PIPE"]) \
|
||||
+ len(connector) \
|
||||
+ len(self._longest_partnumber) \
|
||||
+ len(Treeview.console_colors["RESET"]) \
|
||||
+ len(Treeview.console_colors["RED"]) # other color codes (ansi escape sequences) could have been used instead since they all have the same length
|
||||
|
||||
text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(longest_entry)
|
||||
description = self._descriptions[child_id]
|
||||
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[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__':
|
||||
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('-t', '--tree', action='store_true', help='Erzeugt Baumdarstellung aller Teilenummern. grün = Teilenummer vorhanden, rot = Migration ausstehend')
|
||||
parser.add_argument('-e', '--excel', action='store_true', help='Erzeugt Excel-Datei in der alle Teilenummern, die migriert werden müssen, aufgelistet sind')
|
||||
parser.add_argument('-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')
|
||||
|
||||
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:
|
||||
num = args.sivasid.lstrip()
|
||||
excel_comparison = ExcelExport(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename)
|
||||
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)
|
||||
|
||||
excel_comparison.write_excel()
|
||||
elif args.tree:
|
||||
num = args.sivasid.lstrip()
|
||||
treeview_comparison = Treeview(num, environment.data_dir, environment.data_dir, environment.ruledesigner_csv_filename)
|
||||
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()
|
||||
parser.print_help()
|
||||
+19
-30
@@ -2,8 +2,7 @@ import os
|
||||
from tkinter import messagebox
|
||||
import update_database
|
||||
|
||||
from manage_configs import SivasAssemblyData
|
||||
from get_teilestamm import FetchSivasTeilestamm
|
||||
from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus
|
||||
|
||||
"""
|
||||
Contains the logic for the MVC of CreoMigrate Gui
|
||||
@@ -96,6 +95,8 @@ class Controller(Observer):
|
||||
ret = self._environment.cfg_dir
|
||||
elif env_path == "log":
|
||||
ret = self._environment.log_dir
|
||||
elif env_path == "rd_dbase_filename":
|
||||
ret = self._environment.ruledesigner_csv_filename
|
||||
else:
|
||||
raise Exception("this path is not defined "+env_path)
|
||||
return ret
|
||||
@@ -114,36 +115,26 @@ class Controller(Observer):
|
||||
orange[red] = True
|
||||
return orange
|
||||
|
||||
def get_csv_files_of_asm(self, asmid, uselocal):
|
||||
""" sich die Einzelteile als .csv und den Aufbau des Assemblies als .json holen """
|
||||
def get_csv_files_of_asm(self, asm_id, load_local_asm_json, load_local_rd_dbase):
|
||||
""" Holt sich die Metadaten der Stücklistenpositionen als CSV"""
|
||||
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
|
||||
sivas_teilestamm.fetch_from_db(uselocal)
|
||||
if not uselocal:
|
||||
asm_data = SivasAssemblyData(data_dir, asm_id + '.json', load_local_asm_json)
|
||||
rd_data = RuleDesignerDatabaseConfig(data_dir, rd_dbase_filename, load_local_rd_dbase)
|
||||
migration_status = MigrationStatus(asm_data.child_ids, rd_data.ids)
|
||||
|
||||
if not load_local_asm_json:
|
||||
try:
|
||||
sivas_teilestamm.get_unmigrated_teilstamm_csvs()
|
||||
update_database.get_sivas_teilestamm(migration_status.ids_pending, data_dir)
|
||||
except:
|
||||
self.message_error('Abfrage hat nicht funktioniert', 'sivas_db_error')
|
||||
return
|
||||
|
||||
redids = sivas_teilestamm.get_red_ids()
|
||||
|
||||
sad = SivasAssemblyData(data_dir, asmid + '.json')
|
||||
parent_children = sad.get_parent_children()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
def set_model(self, model):
|
||||
self._model = model
|
||||
@@ -162,15 +153,13 @@ class Controller(Observer):
|
||||
|
||||
def get_current_asm_id(self):
|
||||
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():
|
||||
return
|
||||
# TODO save current given data. In case, ask whether you should save or drop
|
||||
self._model.set_asm_id(asm_id)
|
||||
# hole alle Daten vom Server
|
||||
self.get_csv_files_of_asm(asm_id, loadlocal)
|
||||
self.get_bom_properties(asm_id)
|
||||
#self._load_cid_data2model(asm_id)
|
||||
self.get_csv_files_of_asm(asm_id, load_local_asm_json, load_local_rd_dbase)
|
||||
|
||||
def set_current_item_id(self, cid):
|
||||
"""setze eine neue Item ID """
|
||||
@@ -301,7 +290,7 @@ class Controller(Observer):
|
||||
elif key == 'cb_ladelokal_asm':
|
||||
current_asmid = self.data_from_view('asm_id')
|
||||
# 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':
|
||||
self.suche_3d()
|
||||
|
||||
|
||||
@@ -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()
|
||||
+253
-109
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import json
|
||||
from configparser import ConfigParser, RawConfigParser
|
||||
import pymssql
|
||||
from configparser import ConfigParser
|
||||
import pandas as pd
|
||||
import update_database
|
||||
|
||||
@@ -10,15 +11,20 @@ Diese Lib verwaltet alle Config dateien (als .cfg oder als .json) von CreoMigrat
|
||||
"""
|
||||
class JsonBasedConfig:
|
||||
"""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"""
|
||||
self._config_dir = config_dir
|
||||
self._config_file = config_file
|
||||
self._config_path = os.path.join(config_dir, config_file)
|
||||
|
||||
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"""
|
||||
@@ -27,8 +33,10 @@ class JsonBasedConfig:
|
||||
|
||||
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"""
|
||||
self._config_dir = config_dir
|
||||
self._config_file = config_file
|
||||
self._config_path = os.path.join(config_dir, config_file)
|
||||
|
||||
def load_csv_from_file(self):
|
||||
@@ -36,156 +44,294 @@ 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"""
|
||||
|
||||
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)
|
||||
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):
|
||||
"""Sorgt dafür, dass json-Datei der Baugruppe nur 1x geladen wird"""
|
||||
def create_asm_json(self):
|
||||
"""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):
|
||||
if self.assembly_data is None:
|
||||
self.assembly_data = self.load_json_from_file()
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
@property
|
||||
def asm_data(self) -> dict:
|
||||
"""Liest die Baugruppen-JSON ein und gibt deren Informationen zurück"""
|
||||
if self._asm_data is None:
|
||||
|
||||
@_lazy_init_assembly_data
|
||||
def get_assembly_ids(self):
|
||||
if not self._load_local_json:
|
||||
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)"""
|
||||
|
||||
assembly_ids = set()
|
||||
for child_id, parent_properties in self.assembly_data.items():
|
||||
assembly_ids.add(child_id)
|
||||
for parent_property in parent_properties:
|
||||
parent_id = parent_property["Parent"]
|
||||
assembly_ids.add(parent_id)
|
||||
return assembly_ids
|
||||
if self._child_ids is None:
|
||||
self._child_ids = set()
|
||||
for child_id, parent_properties in self.asm_data.items():
|
||||
self._child_ids.add(child_id)
|
||||
for parent_property in parent_properties:
|
||||
parent_id = parent_property["Parent"]
|
||||
self._child_ids.add(parent_id)
|
||||
return self._child_ids
|
||||
|
||||
@_lazy_init_assembly_data
|
||||
def get_parent_children(self) -> dict:
|
||||
@property
|
||||
def parent_children(self) -> dict:
|
||||
"""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 parent_property in parent_properties:
|
||||
parent_id = parent_property["Parent"]
|
||||
child_posnr_in_parent_bom = int(parent_property["Position"].lstrip(".").split("/")[0])
|
||||
for child_id, parent_properties in self.asm_data.items():
|
||||
for parent_property in parent_properties:
|
||||
parent_id = parent_property["Parent"]
|
||||
child_posnr_in_parent_bom = int(parent_property["Position"].lstrip(".").split("/")[0])
|
||||
|
||||
if parent_id not in unordered_parent_children:
|
||||
unordered_parent_children[parent_id] = {}
|
||||
if parent_id not in unordered_parent_children:
|
||||
unordered_parent_children[parent_id] = {}
|
||||
|
||||
unordered_parent_children[parent_id][child_posnr_in_parent_bom] = child_id
|
||||
|
||||
ordered_parent_children = {}
|
||||
for parent, children in unordered_parent_children.items():
|
||||
for pos_nr, child in sorted(children.items()):
|
||||
if parent not in ordered_parent_children:
|
||||
ordered_parent_children[parent] = []
|
||||
ordered_parent_children[parent].append(child)
|
||||
return ordered_parent_children
|
||||
unordered_parent_children[parent_id][child_posnr_in_parent_bom] = child_id
|
||||
|
||||
ordered_parent_children = {}
|
||||
for parent, children in unordered_parent_children.items():
|
||||
for pos_nr, child in sorted(children.items()):
|
||||
if parent not in ordered_parent_children:
|
||||
ordered_parent_children[parent] = []
|
||||
ordered_parent_children[parent].append(child)
|
||||
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"""
|
||||
|
||||
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
|
||||
def get_max_bom_depth(self) -> int:
|
||||
@property
|
||||
def max_bom_depth(self) -> int:
|
||||
"""Gibt die tiefste Stücklistenebene zurück (0 = höchste Ebene)"""
|
||||
|
||||
bom_levels = []
|
||||
for parents in self.assembly_data.values():
|
||||
for parent in parents:
|
||||
current_bom_level = parent["Position"].count(".")
|
||||
bom_levels.append(current_bom_level)
|
||||
return max(bom_levels)
|
||||
if self._max_bom_depth is None:
|
||||
bom_levels = []
|
||||
for parents in self.asm_data.values():
|
||||
for parent in parents:
|
||||
current_bom_level = parent["Position"].count(".")
|
||||
bom_levels.append(current_bom_level)
|
||||
self._max_bom_depth = max(bom_levels)
|
||||
return self._max_bom_depth
|
||||
|
||||
@_lazy_init_assembly_data
|
||||
def get_property_names(self) -> list:
|
||||
@property
|
||||
def property_names(self) -> list:
|
||||
"""Gibt alle Eigenschaftsnamen zurück, die in der Assembly-json-Datei enthalten sind"""
|
||||
|
||||
for parent_data in self.assembly_data.values():
|
||||
return list(parent_data[0])
|
||||
if self._property_names is None:
|
||||
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:
|
||||
"""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!")
|
||||
|
||||
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:
|
||||
if property in properties:
|
||||
id_property_mapping[child] = properties[property]
|
||||
return id_property_mapping
|
||||
|
||||
@_lazy_init_assembly_data
|
||||
def get_all_bom_properties(self) -> dict:
|
||||
@property
|
||||
def all_bom_properties(self) -> dict:
|
||||
"""Gibt alle Eigenschaften zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
|
||||
|
||||
all_properties = {}
|
||||
property_names = self.get_property_names()
|
||||
for property_name in property_names:
|
||||
all_properties[property_name] = self.get_bom_property_values(property_name)
|
||||
return all_properties
|
||||
if self._all_bom_properties is None:
|
||||
self._all_bom_properties = {}
|
||||
for property_name in self.property_names:
|
||||
self._all_bom_properties[property_name] = self.get_bom_property_values(property_name)
|
||||
return self._all_bom_properties
|
||||
|
||||
|
||||
class RuleDesignerDatabaseConfig(CSVBasedConfig):
|
||||
"""Auslesen der Angaben aus der RuleDesigner-Datenbank-CSV"""
|
||||
def __init__(self, config_dir=".", config_file="RD_Teilenummern.csv"):
|
||||
"""Auslesen der CSV-Datei, in der alle RuleDesigner-Teilenummern aufgelistet sind"""
|
||||
def __init__(self, config_dir=".", config_file="RD_Teilenummern.csv", load_local_csv=False):
|
||||
super().__init__(config_dir, config_file)
|
||||
self._load_local_csv = load_local_csv
|
||||
self._ids = None
|
||||
|
||||
def get_ruledesigner_ids(self):
|
||||
return tuple(self.load_csv_from_file()['Teilenummern'])
|
||||
def copy_csv_from_server(self):
|
||||
"""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:
|
||||
def __init__(self, file_dir, assembly_json):
|
||||
self._file_dir = file_dir
|
||||
self._assembly_json = assembly_json
|
||||
self._uselocal = False
|
||||
"""Klasse gibt Informationen zum aktuellen Migrationsstand zurück, indem geprüft wird, ob die Nummern aus Sivas, auch in RuleDesigner vorliegen"""
|
||||
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._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"""
|
||||
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 use_local(self):
|
||||
self._uselocal = True
|
||||
@property
|
||||
def ids_in_rd(self) -> list:
|
||||
"""Gibt alle Stücklistenpositionen zurück, die migriert sind"""
|
||||
|
||||
def get_migration_status(self) -> dict:
|
||||
"""Gibt zurück, ob die Teilenummern der Baugruppe bereits migriert wurden, oder nicht"""
|
||||
out_dir = self._file_dir
|
||||
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()
|
||||
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
|
||||
|
||||
migration_status = {}
|
||||
for sivas_assembly_id in sivas_assembly_ids:
|
||||
migration_status[sivas_assembly_id] = True if sivas_assembly_id in ruledesigner_ids else False
|
||||
return migration_status
|
||||
@property
|
||||
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"""
|
||||
|
||||
def get_migrated_ids(self) -> list:
|
||||
"""Gibt alle Teilenummern aus der Baugruppenstückliste zurück, die bereits in RuleDesigner migriert wurden"""
|
||||
|
||||
migrated_ids = [assembly_id for assembly_id, migration_status in self.get_migration_status().items() if migration_status is True]
|
||||
return migrated_ids
|
||||
if self._ids_in_progress is None:
|
||||
|
||||
def get_unmigrated_ids(self) -> list:
|
||||
"""Gibt alle Teilenummern aus der Baugruppenstückliste zurück, deren Migration in RuleDesigner noch aussteht"""
|
||||
rd_sql_data = RuleDesignerSQLData()
|
||||
rd_statuses = rd_sql_data.rd_statuses
|
||||
|
||||
unmigrated_ids = [assembly_id for assembly_id, migration_status in self.get_migration_status().items() if migration_status is False]
|
||||
return unmigrated_ids
|
||||
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):
|
||||
@@ -525,7 +671,7 @@ class Configs:
|
||||
|
||||
self.TOSGruppen = TOSGroupsConfig(cfg_dir)
|
||||
self.Materialien = MaterialTransConfig(cfg_dir)
|
||||
self._TeileNummern = None
|
||||
self._teilenummern = None
|
||||
self._blacklist = None
|
||||
|
||||
def get_cfg_path(self):
|
||||
@@ -539,9 +685,9 @@ class Configs:
|
||||
def get_ruledesigner_ids(self):
|
||||
if not self._data_dir:
|
||||
raise Exception("set_data_dir not called. path not set")
|
||||
if not self._TeileNummern:
|
||||
self._TeileNummern = RuleDesignerDatabaseConfig(self._data_dir).get_ruledesigner_ids()
|
||||
return self._TeileNummern
|
||||
if not self._teilenummern:
|
||||
self._teilenummern = RuleDesignerDatabaseConfig(self._data_dir, load_local_csv=True).ids
|
||||
return self._teilenummern
|
||||
|
||||
def get_blacklist(self):
|
||||
if not self._data_dir:
|
||||
@@ -551,5 +697,3 @@ class Configs:
|
||||
lines = f.read().splitlines()
|
||||
self._blacklist = lines
|
||||
return self._blacklist
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
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_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")
|
||||
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"""
|
||||
|
||||
sivas_teilestamm_exe = os.environ.get('SIVAS_TEILESTAMM')
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user