Compare commits

...

10 Commits

Author SHA1 Message Date
s.steuer 2a601ec3c1 In CreoMigrate (GUI-Applikation) können nun auch Teilenummern ohne Stückliste migriert werden. Hierfür wurde in Methode 'get_csv_files_of_asm' von 'controller.py' ein Aufruf der Methode 'extend_asm_bom' aus der Klasse 'SivasBOM' in 'manage_configs.py' ergänzt. Innerhalb der Methode wird die angegebene Teilenummer aus dem Eingabefeld in der GUI, d.h. die Hauptnummer in die Stücklistendaten aufgenommen - standardmäßig ist sie nicht enthalten. Als Oberbaugruppe dieser Nummer wird dabei die fiktive Baugruppe 'None' definiert. Durch diese Ergänzung in den Stücklistendaten wird eine Teilenummer ohne Stückliste also selbst zu einem Bestandteil der fiktiven Baugruppe 'None'. Somit wird also wie bisher nur mit Baugruppen gearbeitet, wodurch die Grundlogik zur Erstellung des Treeviews im Programm gleich bleibt. Lediglich die Vorgehensweise, wenn parent = 'None' gilt musste in den Treeview-Methoden angepasst werden. Im Zuge dessen wurde auch die Methode '_has_childs' der Treeview-Klasse in 'gui.py' angepasst, um sie effizienter zu machen. Konkret wird nun nicht mehr jedes mal durch das parent_children_mapping-dict gelooped werden, um herauszufinden, ob die angegebene Nummer enthalten ist. Da alle Nummern, die als Key in genanntem Dict vorliegen immer Kinder haben muss stattdessen nur geprüft werden, ob die Nummer als Key vorliegt. 2025-04-20 17:40:52 +02:00
s.steuer f01f08cda9 2. Backslash ergänzt, damit 1. Backslash korrekt interpretiert wird 2025-04-20 12:22:49 +02:00
s.steuer 832aaf2088 Variablennamen wurden in 'manage_configs.py' geändert (siehe commit 623f4a1) -> wurde in dieser Datei nachgezogen. Zudem in Methode 'extend_asm_bom' den Key 'Parent' mit aufgenommen, damit die Methode 'parent_children' in 'manage_configs.py' funktioniert. Dadurch kann nun auch der Migrationsstatus von Nummern ohne Stückliste im Treeview korrekt dargestellt werden. 2025-04-20 12:12:08 +02:00
s.steuer 623f4a12e1 In 'manage_configs.py' Klasse 'SivasAssemblyData' in 'SivasBOM' und Klasse 'TeilestammMetadata' in 'SivasTeilestamm' umbenannt, da eindeutiger. In 'controller.py' Namen entsprechend aktualisiert. Zudem in 'manage_configs.py' Klasse 'SivasBOM' die Methode 'asm_data' in 'bom_data' umbenannt. Darüber hinaus gibt die Methode nun ein leeres Dict zurück, wenn keine JSON vorliegt oder keine generiert werden konnte. Dies wurde vorgesehen, um im Migrationsüberblick (Treeview) sowie dem Migrationstool (CreoMigrate) auch Teilenummern ohne Stückliste, d.h. ohne JSON abbilden zu können (genaue Funktionsweise siehe mehrzeiliger Kommentar in genannter Methode). Abgesehen davon Methode 'property_names' komplett entfernt, da diese fehlerhaft funktioniert hat, da jedes Mal durch alle Kinder der Assembly geloopt wurde und die Eigenschaften des aktuellen Kindes wie z.B. 'Bezeichnung', 'Parent', etc. ausgelesen wurden. Der Fehler liegt darin, dass die Eigenschaften der Nummer aus der vorherigen Iteration komplett überschrieben, aber nicht ergänzt wurden, falls in der aktuellen Iteration Eigenschaften vorliegen, die in der vorherigen noch nicht vorlagen. Um dieses Problem ganz zu umgehen, wurden die Methoden 'get_prop_val_for_all_ids' und 'all_bom_properties' umgeschrieben, sodass die Eigenschaften direkt innerhalb dieser Funktionen geholt werden 2025-04-20 12:05:46 +02:00
s.steuer 56ff5010c7 In Funktion 'get_sivas_bom' wird nun in Fällen, bei denen keine JSON erzeugt werden, ein ValueError geschmissen, der ggf. später bei Bedarf per Exception gehandelt werden kann + In Funktion 'get_sivas_teilestamm' wird nun geprüft, ob das Tool 'KSbExcelExportSivasTeilestamm.exe' von Hr. Jakob mit den angegebenen Nummern tatsächlich valide CSV's generiert hat. Das ist deshalb von Bedeutung, weil das Tool auch dann CSV's erzeugt, wenn die Werte, die man per Flag '-teilenr' bereitstellt, nicht als Nummer in SIVAS vorliegen, wie z.B. 'abc' 2025-04-20 11:43:07 +02:00
s.steuer 0c54145655 In update_database.py Funktion 'get_sivas_dbase' in 'get_sivas_bom' umbenannt, da mit dieser Funktion ausschließlich die Stückliste geholt wird und somit spezifischer ist. In der Datei 'manage_configs.py' Funktionen folglich analog umbenannt. Ansonsten Funktionen in 'update_database.py' denested, um Lesbarkeit zu verbessern und Print-Statements in Deutsch geändert, da aktuell ein Mix aus Deutsch und Englisch vorliegt, was zu Verwirrung führen kann. Zudem Docstrings ergänzt und Fehler in den Print-statements behoben. Keine funktionalen Änderungen vorgenommen 2025-04-19 16:40:53 +02:00
s.steuer 3393e7ce47 Umgebungsvariable 'SIVAS_DATABASE_QUERY' in 'SIVAS_BOM' umbenannt, weil auch für die Generierung der Metadaten-CSV eine 'Database-Query' durchgeführt wird, d.h. Bezeichnung ist mehrdeutig. 'SIVAS_BOM' deshalb, weil mit dem Programm 'Sivas2Json', dessen Pfad in 'SIVAS_BOM' gespeichert ist, die Stückliste einer Baugruppe als JSON ausgegeben wird. Passt damit bezeichnungstechnisch zur Umgebungsvariable 'SIVAS_TEILESTAMM', weil mit dem Programm, dessen Pfad in genannter Umgebungsvariable gespeichert ist die Teilstamm-CSV generiert wird. 2025-04-18 23:08:22 +02:00
s.steuer 807b6c79f4 Datei redundant, da die Erstellung der Metadaten-CSV des Teilestamms bereits über die Funktion 'get_teilestamm.py' in 'update_database.py' erfolgt. 2025-04-18 22:53:45 +02:00
s.steuer 80000c15c3 Die Stücklistendaten, die aus der JSON-Datei des Programms 'Sivas2Json' erzeugt werden, wird jetzt immer die Teilenummer der Hauptbaugruppe ergänzt. Dadurch kann die Methode '_print_tree' auch die Nummer der Hauptbaugruppe schreiben. Bisher war hierfür die separate Methode '_tree_header' erforderlich. Für die Aufnahme der Hauptbaugruppe in die Stücklistendaten ist die neue Methode '_add_asm_id_to_bom' zuständig. Die Anpassung wurde vorbereitend für die nächste Erweiterung dieses Programms ergänzt, mit der es möglich sein soll, den Migrationsstand von einzelnen Teilenummern, d.h. Nummern, die keine Baugruppen sind, anzuzeigen. 2025-04-18 22:13:07 +02:00
s.steuer fe302ef950 In der Methode 'child_ids' war es unnötig für die Ermittlung aller Stücklistenpositionen das 'Parent'-Attribut der aufgelisteten Teilenummern in der JSON zu betrachten, da die Teilenummern in der JSON bereits alle Kinder der ASM darstellen / Methode 'extend_asm_bom' ergänzt, damit die Stückliste um eine Nummer ergänzt werden kann (z.B. die Nummer der Oberbaugruppe selbst, da diese aktuell in der JSON nicht ausgegeben wird). Dies wurde ergänzt, damit zukünftig auch der Migrationsstand von einzelnen Teilenummern, die keine Baugruppen sind, angezeigt werden kann, bzw. einzelne Teilenummern migriert werden können. Die entsprechende Anpassung des Treeview- bzw. GUI-Programms erfolgt in einem späteren Commit. Zudem wurde die Methode 'get_prop_val_for_single_id' ergänzt, da im Treeview-Programm bisher bei jedem Eintrag des Baums die Methode 'get_prop_val_for_all_ids' aufgerufen wurde, in der jedes Mal eine bestimmte Eigenschaft, wie z.B. 'Bezeichnung' für alle Stücklistenpositionen der Hauptbaugruppe ermittelt wurde. Das war sehr ineffizient, weil die Eigenschaft nur für den aktuellen Eintrag, d.h. die aktuelle Teilenummer, die im Baum dargestellt werden soll, benötigt wird, aber nicht für alle Stücklistenpositionen. 2025-04-18 22:03:03 +02:00
7 changed files with 277 additions and 270 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ set RD_DATABASE_NAME=RD_Teilenummern.csv
set BLACKLIST_DATABASE_SOURCE=\\ruledesigner\RDF_Daten\Konfigurator\CustomVaulting
set BLACKLIST_DATABASE_NAME=ERP_BOM.ini
set VBA_MACRO_NAME=vba_format_excel.xlsm
set SIVAS_DATABASE_QUERY=Y:\jit\programme\Sivas2json.exe
set SIVAS_BOM=Y:\jit\programme\Sivas2json.exe
set SIVAS_TEILESTAMM=\\195.243.223.3\sivas\jit\programme\KSbExcelExportSivasTeilestamm.exe
set SIVAS_EXCEL_EXPORT_DIR=%CREMIG_DATA%
set SIEMENS_DATA_PREP_TOOL=C:\Program Files\Siemens\Solid Edge 2023\Program\DataPrepUtility.exe
+53 -47
View File
@@ -1,4 +1,4 @@
from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus, TeilestammMetadata
from manage_configs import SivasBOM, RuleDesignerDatabaseConfig, MigrationStatus, SivasTeilestamm
import argparse
import os
import openpyxl
@@ -25,8 +25,8 @@ class ExcelExport:
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.asm_data = SivasBOM(asm_json_dir, asm_json_filename, load_local_asm_json)
self.asm_metadata = SivasTeilestamm(asm_csv_dir, asm_csv_filename, load_local_asm_csv)
self.rd_data = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename, load_local_rd_dbase)
self._migration_status = None
@@ -75,7 +75,7 @@ class ExcelExport:
sorting = self._create_sorting_string()
migrated_ids = self.migration_status.ids_in_rd
child_descriptions:dict = self.asm_data.get_bom_property_values("Bezeichnung")
child_descriptions:dict = self.asm_data.get_prop_val_for_all_ids("Bezeichnung")
parent_description:str = self.asm_metadata.get_property_values("Bezeichnung1")
descriptions = child_descriptions.copy()
@@ -180,8 +180,8 @@ class Treeview:
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.asm_bom = SivasBOM(asm_json_dir, asm_json_filename, load_local_asm_json)
self.asm_teilestamm = SivasTeilestamm(asm_csv_dir, asm_csv_filename, load_local_asm_csv)
self.rd_data = RuleDesignerDatabaseConfig(rd_dbase_dir, rd_dbase_filename, load_local_rd_dbase)
self.no_blue_ids = no_blue_ids
self._migration_status = None
@@ -190,7 +190,7 @@ class Treeview:
@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)
self._migration_status = MigrationStatus(self.asm_bom.child_ids, self.rd_data.ids)
return self._migration_status
@property
@@ -204,55 +204,54 @@ class Treeview:
if self._longest_entry is None:
self._longest_entry = (self.asm_bom_data.max_bom_depth) * len(Treeview.prefix_types["PIPE"]) \
self._longest_entry = (self.asm_bom.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(self.asm_bom.longest_child_id) \
+ len(Treeview.console_colors["RESET"]) \
+ padding_after_id
return self._longest_entry
def generate_tree(self):
self._tree_header()
self._tree_body()
self._tree_summary()
def _tree_header(self):
def _add_asm_id_to_bom(self):
"""Standardmäßig ist die oberste Baugruppennummer kein Bestandteil der JSON-Datei, in der die Baugruppenstruktur aufgelistet ist und
die über das Programm 'Sivas2Json' von Hr. Jakob generiert wird. Damit der Baum die Bezeichnung der obersten Baugruppennummer im
Baum angezeigt wird ist es jedoch erforderlich, dass die Nummer mitsamt ihrer Bezeichnung in der JSON enthalten ist"""
metadata = {"Bezeichnung": self.asm_teilestamm.get_property_values("Bezeichnung1"),
"Parent": None,
"Position": "10/0"}
"""Prints root number (number for which the user wants to see the tree) to the console"""
self.asm_bom.extend_asm_bom(self.asm_id, metadata)
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")
def generate_tree(self):
self._add_asm_id_to_bom()
self._print_tree()
self._print_summary()
print(f"{text_before_description}{description}")
def _tree_body(self, parent_id=None, prefix=""):
def _print_tree(self, parent_id=None, prefix=""):
"""Prints the body (all partnumbers except root/main-partnumber) of the tree to the console"""
"""Gibt die Baugruppenstruktur in Form eines Baumes in der Konsole aus"""
if parent_id is None:
parent_id = self.asm_id
parent_children = self.asm_bom_data.parent_children
parent_children = self.asm_bom.parent_children
children_ids = parent_children[parent_id]
for index, child_id in enumerate(children_ids):
for i, id in enumerate(children_ids):
is_last_assembly_child = self._last_child_check(index, children_ids)
last_child = self._last_child_check(i, children_ids)
connector_symbol = self._set_connector_symbol(id, last_child)
background_color = self._set_background_color(id)
connector_symbol = self._set_connector_symbol(is_last_assembly_child)
partnumber_background_color = self._set_text_background_color(child_id)
self._print_entry(id, prefix, connector_symbol, background_color)
self._print_entry(child_id, prefix, connector_symbol, partnumber_background_color)
if id in parent_children:
next_prefix = self._set_next_prefix(prefix, id, last_child)
self._print_tree(parent_id=id, prefix=next_prefix)
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):
def _print_summary(self):
amount_in_rd = len(self.migration_status.ids_in_rd)
amount_pending = len(self.migration_status.ids_pending)
@@ -261,7 +260,7 @@ class Treeview:
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)
progress = int(amount_in_rd / len(self.asm_bom.child_ids) * 100)
column_width = 5
border_padding = "".ljust(column_width, "")
@@ -299,36 +298,44 @@ class Treeview:
"""Eintrag in die Konsole drucken"""
text_before_description = f"{prefix}{connector}{color}{child_id}{Treeview.console_colors['RESET']}".ljust(self.longest_entry)
description = self.asm_bom_data.get_bom_property_values("Bezeichnung")[child_id]
description = self.asm_bom.get_prop_val_for_single_id(child_id, "Bezeichnung")
print(f"{text_before_description}{description}")
def _last_child_check(self, child_index, children):
def _last_child_check(self, child_idx, children_ids):
"""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_idx == len(children_ids) - 1 else False
def _set_connector_symbol(self, child_is_last_child):
def _set_connector_symbol(self, child_id, last_child):
"""Legt Verbindungssymbol für aktuellen Eintrag im Baum fest"""
return Treeview.connector_symbols["ELBOW"] if child_is_last_child else Treeview.connector_symbols["T"]
if child_id == self.asm_id: # Oberste Baugruppennummer wird immer ohne Verbindersymbol dargestellt
return ""
if not last_child:
return Treeview.connector_symbols["T"]
return Treeview.connector_symbols["ELBOW"]
def _prefix(self, prefix, child_is_last_child):
def _set_next_prefix(self, prefix, child_id, last_child):
"""Erstellt für jeden Eintrag im Baum einen Präfix (Teil vor dem Verbindersymbol) fest"""
"""Legt den Teil VOR dem Verbindersymbol für den nächsten Eintrag im Baum fest"""
prefix += Treeview.prefix_types["EMPTY"] if child_is_last_child else Treeview.prefix_types["PIPE"]
if child_id == self.asm_id: # Erste Stücklistenposition der obersten Baugruppennummer hat nie Präfix
return ""
prefix += Treeview.prefix_types["EMPTY"] if last_child else Treeview.prefix_types["PIPE"]
return prefix
def _set_text_background_color(self, id):
def _set_background_color(self, id):
"""Legt Hintergrundfarbe der Teilenummer basierend auf ihrem Migrationsstatus fest:
@@ -397,7 +404,6 @@ if __name__ == '__main__':
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()
+14 -13
View File
@@ -2,7 +2,7 @@ import os
from tkinter import messagebox
import update_database
from manage_configs import SivasAssemblyData, RuleDesignerDatabaseConfig, MigrationStatus
from manage_configs import SivasBOM, SivasTeilestamm, RuleDesignerDatabaseConfig, MigrationStatus
"""
Contains the logic for the MVC of CreoMigrate Gui
@@ -115,26 +115,27 @@ class Controller(Observer):
orange[red] = True
return orange
def get_csv_files_of_asm(self, asm_id, load_local_asm_json, load_local_rd_dbase):
def get_csv_files_of_asm(self, asm_id, load_local_asm_json=False, load_local_rd_dbase=False):
""" Holt sich die Metadaten der Stücklistenpositionen als CSV"""
data_dir = self.get_env("data")
rd_dbase_filename = self.get_env("rd_dbase_filename")
asm_data = SivasAssemblyData(data_dir, asm_id + '.json', load_local_asm_json)
sivas_bom = SivasBOM(data_dir, asm_id + '.json', load_local_asm_json)
sivas_bom.extend_asm_bom(asm_id, {"Parent": None, "Position": "10/0"}) # Ergänzt Stücklistendaten um eine fiktive Baugruppe "None", in der die Haupt-Teilenummer als Pos. 1 vorliegt. Dadurch können auch Teilenummern ohne Stückliste korrekt abgebildet werden
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:
update_database.get_sivas_teilestamm(migration_status.ids_pending, data_dir)
except:
self.message_error('Abfrage hat nicht funktioniert', 'sivas_db_error')
return
migration_status = MigrationStatus(sivas_bom.child_ids, rd_data.ids)
pending_ids = migration_status.ids_pending
self._model.set_red_ids(migration_status.ids_pending)
self._model.set_parent_children(asm_data.parent_children)
sivas_teilestamm = SivasTeilestamm(data_dir, asm_id + '.csv')
sivas_teilestamm.create_csvs(pending_ids)
self._model.set_red_ids(pending_ids)
self._model.set_parent_children(sivas_bom.parent_children)
self._model.set_asm_id(asm_id)
self._model.set_bom_properties(asm_data.all_bom_properties)
self._model.set_bom_properties(sivas_bom.all_bom_properties)
def set_model(self, model):
self._model = model
-51
View File
@@ -1,51 +0,0 @@
import argparse
import os
import shutil
import subprocess
"""
Dieses Programm:
- Holt sich alle notwendigen Einzelteil-Informationen aus SIVAS in Tabellenform
"""
def get_sivas_teilestamm(sivas_id, out_dir):
sivas_id = sivas_id.strip()
sivas_teilestamm_exe = os.environ.get('SIVAS_TEILESTAMM')
if os.path.isfile(sivas_teilestamm_exe):
if os.path.isdir(out_dir):
print(f"Attempting to generate a json-file of all parts to be migrated in relation with '{sivas_id}'")
source_path = os.path.join(os.path.expandvars("%USERPROFILE%"), "AppData", "Local", "Temp", "from_siv.csv")
to_path = os.path.join(out_dir, sivas_id + ".csv")
if os.path.isfile(os.path.join(to_path)):
print("file already there. done")
exit()
subprocess.run(f"{sivas_teilestamm_exe} userid=sivas password=forte database=sivas program=ExcelUeb LFDNR=500 EINSTIEG=SQL PARAMETER1={sivas_id}")
if os.path.isfile(os.path.join(source_path)):
shutil.move(source_path, to_path)
print("done")
else:
print(f"Failure - Couldn't generate {sivas_id}.json. Check for any spelling/typing errors. Otherwise given number is a part without a BOM.")
exit()
else:
print(f"'Output directory for .json-file {out_dir}' doesn't exist.")
else:
print(f"Can't execute SIVAS-database query. Check if you have established a VPN connection to the company's network and check the path of %SIVAS_DATABASE_QUERY% in cremig_setenv.bat")
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 data of this id from sivas fo local database folder', metavar='id')
args = parser.parse_args()
out_dir = os.environ.get('CREMIG_DATA')
if args.sivasid:
get_sivas_teilestamm(args.sivasid, out_dir)
else:
parser.print_help()
+12 -13
View File
@@ -55,10 +55,7 @@ class AllFrames():
class TreeView():
def __init__(self, frame, nrow=0):
# a treeview
# hier Anzahl der sichtbaren Zeilen des Baums angeben:
self.tree = ttk.Treeview(frame, height=65)
# TODO hier anstatt root die Statistik rein schreiben.
#self.tree.heading('#0', text='root', anchor=tk.W)
self.tree = ttk.Treeview(frame, height=65) # 'height' steuert Anzahl der sichtbaren Zeilen im Baum
self.tree.grid(row=nrow, column=0, sticky=tk.NSEW)
# add a scrollbar
@@ -75,7 +72,9 @@ class TreeView():
done_items = set()
undone_items = set()
for parent,children in parent_children_mapping.items():
if parent in red_ids:
if parent is None:
pass
elif parent in red_ids:
undone_items.add(parent)
else:
done_items.add(parent)
@@ -108,18 +107,16 @@ class TreeView():
self.tree.delete(c)
def _has_childs(self, sid, parent_children_mapping):
'''gibt zurück ob sid ein ein Zwischenknoten eines Baume ist, also kein Blatt hat'''
for parent,children in parent_children_mapping.items():
if sid == parent:
return True
return False
'''gibt zurück ob sivas-id ein Zwischenknoten eines Baume ist, also kein Blatt hat'''
return True if sid in parent_children_mapping else False
def create(self, parent_children_mapping, red_ids, orange_ids):
pool = dict()
self.set_heading_text(parent_children_mapping, red_ids)
for parent, children in parent_children_mapping.items():
# parent erzeugen, falls es diese nicht schon gibt (check der pool ids)
if parent not in pool:
if parent is None:
parent_id = None
elif parent not in pool:
if parent not in red_ids: rowtags = ('good')
elif parent in orange_ids: rowtags = ('orange')
else: rowtags = ('missing')
@@ -148,7 +145,9 @@ class TreeView():
child_id = self.tree.insert('', tk.END, text=child, tags=rowtags, open=True)
pool[child] = child_id # hier werden die keys - sivas Nummern auch überschrieben
self.trid2sivasId[child_id] = child # merken der eindeutigen Zuordnung für den späteren Doppelklick
self.tree.move(child_id, parent_id, 'end')
if parent_id is not None:
self.tree.move(child_id, parent_id, 'end')
# https://www.askpython.com/python-modules/tkinter/tkinter-colors
#self.tree.tag_configure('good', background='LawnGreen')
+95 -59
View File
@@ -19,12 +19,9 @@ class JsonBasedConfig:
def load_json_from_file(self):
"""lade das json file vom gegeben Pfad in ein json Objekt"""
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.")
with open(self._config_path, 'r', encoding='utf-8') as fh:
content = fh.read()
return json.loads(content)
def from_json(self, json_str):
"""nimm den Json String zur Initialisierung des Objektes"""
@@ -99,50 +96,63 @@ class RuleDesignerSQLData(SQLBasedConfig):
return self._rd_statuses
class SivasAssemblyData(JsonBasedConfig):
class SivasBOM(JsonBasedConfig):
"""Auslesen der Assembly-Informationen zu einer angegebenen Baugruppennummer"""
def __init__(self, config_dir: str, config_file: str, load_local_json=False):
super().__init__(config_dir, config_file)
self._load_local_json = load_local_json
self._asm_data = None
self._bom_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 create_asm_json(self):
def create_asm_json(self) -> bool:
"""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!")
update_database.get_sivas_bom(self._config_file.removesuffix(".json"))
@property
def asm_data(self) -> dict:
"""Liest die Baugruppen-JSON ein und gibt deren Informationen zurück"""
if self._asm_data is None:
def bom_data(self) -> dict:
""" Liest die JSON-Datei der Baugruppenstückliste ein und speichert die Informationen als dict. """
if self._bom_data is None:
"""
Falls die JSON nicht existiert oder nicht generiert werden kann, soll die Möglichkeit bestehen,
diese eigenständig zu erweitern, da in der JSON z.B. die Nummer der obersten Baugruppe standardmäßig
nicht vorliegt. Generierungs- und Einlese-Fehler werden daher bewusst ignoriert, um stattdessen ein leeres
dict zurückzugeben, das anschließend erweitert werden kann.
Die Möglichkeit zur Erweiterung der Stückliste ermöglicht es zudem, Teilenummern ohne Stückliste abzubilden.
Das funktioniert, indem man die Teilenummer ohne eigene Stückliste als Kind einer fiktiven Baugruppe "None"
ansieht / definiert. Dadurch ist keine separate Logik erforderlich, die sich nur mit Teilenummern
ohne Stückliste befasst.
"""
self._bom_data = {}
if not self._load_local_json:
self.create_asm_json()
try:
self.create_asm_json()
except ValueError:
pass
try:
self._bom_data = self.load_json_from_file()
except FileNotFoundError:
pass
self._asm_data = self.load_json_from_file()
return self._asm_data
return self._bom_data
@property
def child_ids(self) -> set:
"""Gibt alle Teilenummern der Stückliste zurück (auch Teilenummern aus tieferen Baugruppenebenen)"""
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)
self._child_ids = set(self.bom_data.keys())
return self._child_ids
@property
@@ -152,7 +162,7 @@ class SivasAssemblyData(JsonBasedConfig):
if self._parent_children is None:
unordered_parent_children = {}
for child_id, parent_properties in self.asm_data.items():
for child_id, parent_properties in self.bom_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])
@@ -170,6 +180,26 @@ class SivasAssemblyData(JsonBasedConfig):
ordered_parent_children[parent].append(child)
self._parent_children = ordered_parent_children
return self._parent_children
def extend_asm_bom(self, id, metadata:dict) -> dict:
"""Ergänzt die Stückliste um eine weitere Teilenummer unter Berücksichtigung des vorgegebenen Formats der ausgegebenen JSON
des Programms 'Sivas2Json' von Hr. Jakob
Aktuell ist die JSON wie folgt aufgebaut:
{
"Stücklistenposition 1": [{Metadaten aus 1. SIVAS-Stammstückliste, in der die Stücklistenposition verbaut ist},
{Metadaten aus 2. SIVAS-Stammstückliste, in der die Stücklistenposition verbaut ist (falls die oberste Baugruppe weitere Unterbaugruppen enthält, in denen diese Nummer ebenfalls verbaut ist)},
...
]
"Stücklistenposition 2": [{...},
{...}
]
}"""
if id not in self.bom_data:
self._bom_data[id] = []
self._bom_data[id].append(metadata)
return self._bom_data
@property
def longest_child_id(self) -> str:
@@ -185,33 +215,32 @@ class SivasAssemblyData(JsonBasedConfig):
if self._max_bom_depth is None:
bom_levels = []
for parents in self.asm_data.values():
for parents in self.bom_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
@property
def property_names(self) -> list:
"""Gibt alle Eigenschaftsnamen zurück, die in der Assembly-json-Datei enthalten sind"""
def get_prop_val_for_single_id(self, id:str, property_name:str) -> str:
"""Gibt eine Eigenschaft von einer einzelnen Teilenummer aus der Stückliste zurück"""
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
def get_bom_property_values(self, property) -> dict:
"""Gibt eine Eigenschaft zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
if id not in self._bom_data:
raise ValueError("Teilenummer befindet sich nicht in der Baugruppe!")
if property not in self.property_names:
raise ValueError("Angegebener Eigenschaftsname exisitert nicht!")
if property_name not in self._bom_data[id][0]:
return
return self._bom_data[id][0][property_name]
def get_prop_val_for_all_ids(self, property_name) -> dict:
"""Gibt eine Eigenschaft zu allen Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
id_property_mapping = {}
for child, parent_data in self.asm_data.items():
for properties in parent_data:
if property in properties:
id_property_mapping[child] = properties[property]
for child_id, asm_usages in self.bom_data.items():
child_properties = asm_usages[0]
id_property_mapping[child_id] = None if property_name not in child_properties else child_properties[property_name]
return id_property_mapping
@property
@@ -219,9 +248,16 @@ class SivasAssemblyData(JsonBasedConfig):
"""Gibt alle Eigenschaften zu den Teilenummern der Stückliste zurück (z.B. Bezeichnung)"""
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)
for child_id, asm_usages in self.bom_data.items():
child_properties = asm_usages[0]
for property_name, property_val in child_properties.items():
if property_name not in self._all_bom_properties:
self._all_bom_properties[property_name] = {}
self._all_bom_properties[property_name][child_id] = property_val
return self._all_bom_properties
@@ -235,7 +271,7 @@ class RuleDesignerDatabaseConfig(CSVBasedConfig):
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()
update_database.get_ruledesigner_ids()
except:
raise ValueError("CSV mit RuleDesigner-Teilenummern konnte nicht ins lokale Verzeichnis kopiert werden!")
@@ -303,34 +339,34 @@ class MigrationStatus:
return self._ids_pending
class TeilestammMetadata(CSVBasedConfig):
class SivasTeilestamm(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
self._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!")
update_database.get_sivas_teilestamm(ids)
@property
def csv_data(self):
def teilestamm_data(self):
"""Gibt CSV-Daten als Dataframe zurück"""
if self._csv_data is None:
if self._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
self._data = self.load_csv_from_file()
return self._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]
ret = self.teilestamm_data[property_name][0]
return ret
+102 -86
View File
@@ -4,23 +4,36 @@ import os
import shutil
import subprocess
import sys
import csv
"""
Dieses Programm:
- Kopiert die CSV-Datei mit allen RuleDesigner-Nummern
- Fragt Sivas ab und generiert dabei eine json-Datei, in der alle Teilenummern aufgelistet sind,
die in Verbindung mit einer angegebenen Teilenummer migriert werden müssen.
Diese Library erzeugt Dateien mit RuleDesigner-, bzw. SIVAS-Informationen,
bzw. stellt diese durch Kopieren / Verschieben von bereits vorhandenen
Dateien auf Netzlaufwerken bereit
"""
def get_blacklist(outdir):
"""read out list of sivas Ids which are not allowed from network drive
'//ruledesigner//DF_Daten//onfigurator//ustomVaulting//RP_BOM.ini'
within the section 'Settings'
in the key 'PartNumbersToDrop'
"""
Schreibt Teilenummern aus der nachfolgenden .ini-Datei in eine .txt-Datei:
-> Dateipfad: //ruledesigner/RDF_Daten/Configurator/CustomVaulting/ERP_BOM.ini'
-> Abschnitt: 'Settings'
-> Schlüssel: 'PartNumbersToDrop'
Bei diesen Teilenummern darf keine Stücklistenübergabe an SIVAS erfolgen, da sich die Stücklistenstruktur
zwischen dem CAD und SIVAS unterscheidet (vorrangig bei Baugruppen mit Lieferantenbeistellungen)
"""
blacklist_net_share = os.environ.get("BLACKLIST_DATABASE_SOURCE")
blacklist_ini_filename = os.environ.get("BLACKLIST_DATABASE_NAME")
blacklist_source_path = os.path.join(blacklist_net_share, blacklist_ini_filename)
config = configparser.RawConfigParser()
if not os.path.isfile(blacklist_source_path):
raise FileNotFoundError(f"Kein Zugriff auf ini-Datei '{blacklist_source_path}' mit den Blacklist-Teilenummern möglich. Bitte $BLACKLIST_DATABASE_SOURCE & $BLACKLIST_DATABASE_NAME in cremig_setenv.bat sowie VPN-Verbindung prüfen")
try:
config.read(blacklist_source_path)
except configparser.DuplicateOptionError:
@@ -28,123 +41,126 @@ def get_blacklist(outdir):
content = config.get('Settings','PartNumbersToDrop')
if type(content) == list:
content = content[0] # Wird benötigt, weil der String mit den Nummern aus der Blackliste manchmal in eine Liste gepackt wird, manchmal nicht - warum wissen die Götter...
if isinstance(content, list):
content = content[0] # Manchmal wird String mit den Blacklist-Nummern in einer Liste geschrieben - warum wissen die Götter...
if len(content) == 0 or len(content) == 0:
print(f"import of '{blacklist_ini_filename}' from '{blacklist_net_share}' failed")
exit
if len(content) == 0:
raise ValueError(f"Öffnen der Blacklist-Datei'{blacklist_ini_filename}' in '{blacklist_net_share}' fehlgeschlagen")
blacklist = content.split(';')
print("writing txt-file of blacklist ids...")
outfile = os.path.join(outdir, 'blacklist.txt')
print(f"Schreibe Blacklist-Teilenummern in '{outfile}'...")
with open(outfile, 'w', encoding='utf-8') as fh:
fh.write('\n'.join(blacklist))
print("done")
def get_rd_dbase(out_dir=os.environ.get('CREMIG_DATA')):
def get_ruledesigner_ids(out_dir=os.environ.get('CREMIG_DATA')):
"""Kopiert CSV-Datei, in der alle RuleDesigner-Teilenummern aufgelistet sind, vom Netzlaufwerk in ein lokales Verzeichnis"""
rd_csv_source_dir = os.environ.get("RD_DATABASE_SOURCE")
rd_csv_filename = os.environ.get("RD_DATABASE_NAME")
csv_source_path = os.path.join(rd_csv_source_dir, rd_csv_filename)
csv_destiny_path = os.path.join(out_dir, rd_csv_filename)
if os.path.isfile(csv_source_path):
csv_destiny_path = os.path.join(out_dir, rd_csv_filename)
if not os.path.isfile(csv_source_path):
raise FileNotFoundError(f"CSV-Datei mit RuleDesigner-Teilenummern existiert nicht unter '{csv_source_path}'. Bitte $RD_DATABASE_SOURCE & $RD_DATABASE_NAME in cremig_setenv.bat prüfen.")
if os.path.isdir(out_dir):
print("Copying CSV-file of RuleDesigner database...")
shutil.copyfile(csv_source_path, csv_destiny_path)
print("done")
else:
print(f"Can't copy RuleDesigner-database, because output directory ${out_dir} doesn't exist. Check %CREMIG_DATA% in cremig_setenv.bat")
sys.exit(0)
else:
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)
if not os.path.isdir(out_dir):
raise NotADirectoryError(f"Zielverzeichnis '{out_dir}' für CSV-Datei mit RuleDesigner-Teilenummern existiert nicht. Bitte $CREMIG_DATA in cremig_setenv.bat und VPN-Verbindung prüfen")
def get_sivas_dbase(sivas_id, out_dir=os.environ.get('CREMIG_DATA')):
sivas_exe_path = os.environ.get('SIVAS_DATABASE_QUERY')
print(f"Kopiere '{rd_csv_source_dir}\\{rd_csv_filename}'...")
shutil.copyfile(csv_source_path, csv_destiny_path)
def get_sivas_bom(sivas_id, out_dir=os.environ.get('CREMIG_DATA')):
"""Generiert JSON-Datei, in der die Stücklisteninformationen zu einer SIVAS-Teilenummer mit Stammstückliste gespeichert sind.
Die Erstellung der JSON-Datei erfolgt unter Verwendung des externen Sivas2Json-Programms von Hr. Jakob"""
sivas2json_exe_path = os.environ.get('SIVAS_BOM')
sivas_id = sivas_id.replace(".json", "")
if os.path.isfile(sivas_exe_path):
if os.path.isdir(out_dir):
print(f"Attempting to generate a json-file of all parts to be migrated in relation with '{sivas_id}'")
subprocess.run(f'{sivas_exe_path} -o {out_dir} --format json -number "{sivas_id}"')
if os.path.isfile(os.path.join(out_dir, sivas_id + ".json")):
print("done")
return True
else:
print(f"Failure - Couldn't generate {sivas_id}.json. Check for any spelling/typing errors. Otherwise given number is a part without a BOM.")
return False
else:
print(f"'Output directory for .json-file {out_dir}' doesn't exist. Check cremig_setenv.bat or VPN connection.")
return False
else:
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
if not os.path.isfile(sivas2json_exe_path):
raise FileNotFoundError(f"Programmpfad '{sivas2json_exe_path}' nicht erreichbar. Bitte prüfen, ob Zugriff auf Laufwerk Y: besteht. Andernfalls $SIVAS_BOM in cremig_setenv.bat sowie VPN-Verbindung prüfen")
if not os.path.isdir(out_dir):
raise NotADirectoryError(f"Ausgabeverzeichnis '{out_dir}' für die Stücklisten-JSON existiert nicht. Bitte $SIVAS_BOM in cremig_setenv.bat oder VPN-Verbindung prüfen.")
print(f"Erstelle JSON mit Stücklisteninformationen...")
subprocess.run(f'{sivas2json_exe_path} -o {out_dir} --format json -number "{sivas_id}"')
if not os.path.isfile(os.path.join(out_dir, sivas_id + ".json")):
raise ValueError(f"{sivas_id}.json konnte nicht erzeugt werden. Evtl. Nummer ohne Stückliste.")
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"""
"""
Generiert für jede angegebene SIVAS-Teilenummer eine CSV-Datei, in der die Teilestamm-Informationen gespeichert sind
Es kann eine oder mehrere Nummern angegeben werden. Mehrere Nummern sind entweder als kommaseparierter String oder
in Form einer Liste anzugeben
"""
sivas_teilestamm_exe = os.environ.get('SIVAS_TEILESTAMM')
if not os.path.isfile(sivas_teilestamm_exe):
raise FileNotFoundError(f"Check VPN connection or path of %SIVAS_DATABASE_QUERY% in cremig_setenv.bat")
raise FileNotFoundError(f"Programmpfad '{sivas_teilestamm_exe}' nicht erreichbar. Bitte prüfen, ob Zugriff auf Laufwerk Y: besteht. Andernfalls $SIVAS_TEILESTAMM in cremig_setenv.bat sowie VPN-Verbindung prüfen")
if not os.path.isdir(out_dir):
raise NotADirectoryError(f"Output directory for .csv-file(s) ${out_dir} doesn't exist.")
raise NotADirectoryError(f"Ausgabeverzeichnis '{out_dir}' für die Teilestamm-CSV existiert nicht. Bitte $SIVAS_TEILESTAMM in cremig_setenv.bat oder VPN-Verbindung prüfen.")
if isinstance(sivas_ids, str):
sivas_ids = list(set(sivas_ids.strip().split(",")))
ids_to_generate = []
for id in sivas_ids:
out_file = os.path.join(out_dir, id + ".csv")
if not overwrite and os.path.isfile(out_file):
if not overwrite and os.path.isfile(os.path.join(out_dir, id + ".csv")):
continue
ids_to_generate.append(f'"{id}"') # Anführungszeichen erforderlich, damit Art.Nr. mit Leerzeichen, z.B. "TOS Auswahl ILS" funktionieren
ids_to_generate.append(id)
if len(ids_to_generate) == 0:
print("CSV-file(s) already exist or given partnumber(s) are migrated.")
print("CSV-Datei(en) existieren bereits oder angegebene Teilenummer(n) sind bereits migriert")
return
ids_to_generate = ",".join(ids_to_generate)
os.environ["SIVAS_EXCEL_EXPORT_DIR"] = out_dir
subprocess.run(f'{sivas_teilestamm_exe} -teilenr={ids_to_generate} -open_target_dir=n')
ids_to_generate_str = ",".join([f'"{id}"' for id in ids_to_generate]) # Anführungszeichen erforderlich, damit Art.Nr. mit Leerzeichen, z.B. "TOS Auswahl ILS" funktionieren
os.environ["SIVAS_EXCEL_EXPORT_DIR"] = out_dir # Umgebungsvariable legt Ausgabeverzeichnis der CSV-Dateien im Programm 'KSbExcelExportSivasTeilestamm' fest. Der Variablenname ist durch genanntes Programm vorgegeben, siehe help-Text des Programms!
print(f"Erstelle CSV(s) mit Teilestamminformationen...")
subprocess.run(f'{sivas_teilestamm_exe} -teilenr={ids_to_generate_str} -open_target_dir=n')
# Prüft Existenz der angegebenen SIVAS-Nummern, indem geschaut wird, ob die CSV-Dateien 2 Zeilen aufweisen
# Nicht über Returncode des Subprocess ermittelbar, da dieser immer 0 ist, auch wenn die Nr. nicht in SIVAS existiert
for id in ids_to_generate:
csv_path = os.path.join(out_dir, id + ".csv")
with open(csv_path, encoding="utf-8", newline="") as csv_file:
csv_data = csv.reader(csv_file, delimiter=";")
line_nums = sum(1 for row in csv_data)
if line_nums != 2:
raise ValueError(f"SIVAS-Teilenummer '{id}' existiert nicht! Bitte prüfen, ob Nummer korrekt geschrieben wurde!")
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 data of this id from sivas fo local database folder', metavar='id')
parser.add_argument('-r', '--rd', action='store_true', help='fetch list of existing parts from network drive')
parser.add_argument('-b', '--blacklist', action='store_true', help='fetch list of not allowed parts from network drive')
parser.add_argument('-t', '--teilestamm', action='store_true', help='fetch teilestamm-data for given partnumbers as csv')
parser.add_argument('-w', '--overwrite', action='store_true', help='vorhandene CSV-Dateien werden nicht erneut aus SIVAS abgefragt')
parser = argparse.ArgumentParser(description='Erzeugt Dateien mit RuleDesigner-, bzw. SIVAS-Informationen, bzw. stellt diese bereit', prog='update_database')
parser.add_argument('-b', '--blacklist', action='store_true', help='Schreibe .txt-Datei mit allen Teilenummern, bei denen keine Stücklistenübergabe aus dem CAD nach SIVAS erfolgen darf')
parser.add_argument('-d', '--out_dir', action='store', default=os.environ.get('CREMIG_DATA'), help='Ausgabeverzeichnis für Datei', metavar="dir")
parser.add_argument('-m', '--bom', action='store_true', help='Schreibe Stückliste einer SIVAS-Teilenummer als JSON-Datei')
parser.add_argument('-o', '--overwrite', action='store_true', default=False, help='Überschreibe Datei, wenn diese bereits in Zielverzeichnis vorliegt')
parser.add_argument('-r', '--rd_ids', action='store_true' ,help='Kopiere CSV-Datei, in der alle RuleDesigner-Teilenummern aufgelistet sind')
parser.add_argument('-s', '--sivasid', action='store', help='SIVAS-Teilenummer', metavar='id')
parser.add_argument('-t', '--teilestamm', action='store_true', help='Schreibe Teilestamm-Medataten einer SIVAS-Teilenummer als CSV-Datei')
args = parser.parse_args()
out_dir = os.environ.get('CREMIG_DATA')
if args.rd:
get_rd_dbase(out_dir)
elif args.sivasid:
if args.teilestamm:
overwrite = args.overwrite
if overwrite:
get_sivas_teilestamm(args.sivasid, out_dir, overwrite = True)
else:
get_sivas_teilestamm(args.sivasid, out_dir, overwrite = False)
else:
get_sivas_dbase(args.sivasid, out_dir)
elif args.blacklist:
get_blacklist(out_dir)
else:
if len(sys.argv) == 1:
parser.print_help()
if args.rd_ids:
get_ruledesigner_ids(args.out_dir)
if args.blacklist:
get_blacklist(args.out_dir)
if args.sivasid:
if args.teilestamm:
get_sivas_teilestamm(args.sivasid, args.out_dir, overwrite = True)
if args.stueckliste:
get_sivas_bom(args.sivasid, args.out_dir)