Compare commits
5 Commits
f33870e553
...
727f629e76
| Author | SHA1 | Date | |
|---|---|---|---|
| 727f629e76 | |||
| d3ab0cc1e5 | |||
| 9fd39731be | |||
| 2cefde24a6 | |||
| d7b1fcb391 |
+2
-162
@@ -1,162 +1,2 @@
|
|||||||
# ---> Python
|
/.venv
|
||||||
# Byte-compiled / optimized / DLL files
|
/work
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+11
-1
@@ -9,7 +9,17 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ConsoleOutputNewBOM",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true,
|
||||||
|
"args": ["000000382", "csv"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
REM ~dp0 steht für das Verzeichnis, in der diese Datei liegt
|
||||||
|
pushd %~dp0\..
|
||||||
|
|
||||||
|
set PROJECT=%cd%
|
||||||
|
set PROJECT_BIN=%PROJECT%\bin
|
||||||
|
set PROJECT_CFG=%PROJECT%\cfg
|
||||||
|
set PROJECT_DOC=%PROJECT%\doc
|
||||||
|
set PROJECT_LIB=%PROJECT%\lib
|
||||||
|
set PROJECT_DATA=%PROJECT%\data
|
||||||
|
set PROJECT_WORK=%PROJECT%\work
|
||||||
|
set PROJECT_TEST=%PROJECT%\testdata
|
||||||
|
|
||||||
|
if not exist %PROJECT%\work mkdir %PROJECT%\work
|
||||||
|
if not exist %PROJECT%\data mkdir %PROJECT%\data
|
||||||
|
|
||||||
|
set DB_PATH=%PROJECT_WORK%\sivas_replika.db
|
||||||
|
set PATH=%PROJECT_BIN%;%PATH%
|
||||||
|
|
||||||
|
popd
|
||||||
|
goto :eof
|
||||||
+466
-280
@@ -2,78 +2,361 @@ import argparse
|
|||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from contextlib import closing
|
|
||||||
|
|
||||||
def main(asm_partnum, sql_filedir, csv_filedir):
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Programm bearbeitet Stücklisten-CSV aus RuleDesigner, sodass Spaltenüberschreibungen + TOS-Bedingungen und -Regeln (falls vorhanden) mit verschoben werden',
|
||||||
|
prog='verschiebelogik',
|
||||||
|
formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=50))
|
||||||
|
parser.register("action", "store_after_check", ArgparseDirectoryCheck)
|
||||||
|
parser.add_argument("parent_code", action="store", type=str, help="Teilenummer der Baugruppe")
|
||||||
|
parser.add_argument("-cid", "--in_csv_dir", action="store_after_check", default=os.environ.get("PROJECT_DATA"), help="Eingabeverzeichnis der Stücklisten-CSV aus RuleDesigner")
|
||||||
|
subparsers = parser.add_subparsers(title="output type", help="Ausgabe der fertigen Stückliste in Konsole oder als CSV", dest="output_type")
|
||||||
|
sp_cmd = subparsers.add_parser("console", help="Ausgabe der fertig bearbeiteten Stückliste in der Konsole")
|
||||||
|
sp_csv = subparsers.add_parser("csv", help="Ausgabe der fertig bearbeiteten Stückliste als CSV-Datei")
|
||||||
|
sp_csv.add_argument("-v", "--verbose", action="store_true", help="Alle Spalten ausgeben")
|
||||||
|
sp_csv.register("action", "store_after_check", ArgparseDirectoryCheck)
|
||||||
|
sp_cmd.add_argument("-v", "--verbose", action="store_true", help="Alle Spalten ausgeben")
|
||||||
|
sp_csv.add_argument("-cod", "--out_csv_dir", action="store_after_check", default=os.environ.get("PROJECT_WORK"), help="Ausgabeverzeichnis fertigen Stücklisten-CSV")
|
||||||
|
args: dict = vars(parser.parse_args())
|
||||||
|
|
||||||
build_sivas_from_file(sql_filedir)
|
if os.environ.get("DB_PATH") is not None:
|
||||||
csv_data = read_csv(csv_filedir, asm_partnum)
|
# nur 1 Datenbankinstanz über gesamte Laufzeit (ähnlich Singleton)
|
||||||
csv_to_db(csv_data)
|
db = Database(os.environ.get("DB_PATH"))
|
||||||
create_new_bom(asm_partnum)
|
else:
|
||||||
# print_new_bom(asm_partnum)
|
raise ValueError("Bitte bin/setenv.bat vor diesem Programm für erforderliche Umgebungsvariablen ausführen!")
|
||||||
write_bom_as_csv(asm_partnum)
|
|
||||||
|
|
||||||
def csv_to_db(csv_data: list[dict[str, str]]) -> None:
|
try:
|
||||||
"""Schreibt neue Stücklistendaten aus CSV in temporäre Datenbanktabelle."""
|
old_bom = COMP(db)
|
||||||
|
new_bom = RD_COMP(db, args["parent_code"])
|
||||||
|
csv_handler = CSVHandler()
|
||||||
|
sql_handler = SQLFileHandler()
|
||||||
|
bom_processing = BOMProcessing(db, args["parent_code"])
|
||||||
|
|
||||||
con.execute("""CREATE TABLE RD_COMP (ParentCode TEXT NOT NULL,
|
# SIVAS "nachbauen"
|
||||||
Positionsnummer INTEGER NOT NULL,
|
sql_source_filename = "sivas_replika.sql"
|
||||||
ChildCode TEXT NOT NULL,
|
sivas_source_data = sql_handler.read_from_file(filename=sql_source_filename)
|
||||||
Menge INTEGER NOT NULL,
|
old_bom.rebuild_sivas(sivas_source_data)
|
||||||
Uebergabelaenge REAL NOT NULL,
|
|
||||||
PRIMARY KEY(Positionsnummer))
|
|
||||||
""")
|
|
||||||
|
|
||||||
for line in csv_data:
|
# CSV-Daten in Datenbank schreiben
|
||||||
con.execute(""" INSERT INTO RD_COMP (ParentCode, Positionsnummer, ChildCode, Menge, Uebergabelaenge)
|
csv_data = csv_handler.read_as_dict(filename=f"BOM_{args["parent_code"]}")
|
||||||
VALUES (:ParentCode, :Positionsnummer, :ChildCode, :Menge, :Uebergabelaenge)""", line)
|
new_bom.create()
|
||||||
|
new_bom.insert_csv_data(csv_data)
|
||||||
|
|
||||||
|
# Neue Stückliste erstellen / schreiben
|
||||||
|
bom_processing.write_new_bom()
|
||||||
|
|
||||||
|
if args["output_type"] == "console":
|
||||||
|
if args["verbose"]:
|
||||||
|
output = repr(new_bom)
|
||||||
|
print(output)
|
||||||
|
else:
|
||||||
|
print(new_bom)
|
||||||
|
elif args["output_type"] == "csv":
|
||||||
|
if args["verbose"]:
|
||||||
|
bom_data = new_bom.get_verbose_data()
|
||||||
|
else:
|
||||||
|
bom_data = new_bom.get_compact_data()
|
||||||
|
|
||||||
|
csv_handler.write_from_dict(data=bom_data,
|
||||||
|
filename=f"BOM_{args["parent_code"]}_final",
|
||||||
|
filedir=args["out_csv_dir"])
|
||||||
|
else:
|
||||||
|
NotImplementedError
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
db.remove() # Damit bei jedem Lauf eine neue erstellt wird -> mit :memory: traten Bugs auf
|
||||||
|
|
||||||
|
|
||||||
|
class ArgparseDirectoryCheck(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values: list[str], option_string = None):
|
||||||
|
if not os.path.isdir(values[0]):
|
||||||
|
raise NotADirectoryError
|
||||||
|
setattr(namespace, self.dest, values[0])
|
||||||
|
|
||||||
|
class FileHandler:
|
||||||
|
def check_filedir(self, filedir: str) -> str:
|
||||||
|
if filedir is None:
|
||||||
|
raise ValueError("Bitte bin/setenv.bat ausführen, oder Verzeichnis angegeben, in dem die Eingabedatei liegt!")
|
||||||
|
if not os.path.isdir(filedir):
|
||||||
|
raise NotADirectoryError
|
||||||
|
return filedir
|
||||||
|
|
||||||
|
def check_filepath(self, filepath: str) -> str:
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
raise FileNotFoundError("Angegebener Dateiname existiert nicht!")
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
class CSVHandler(FileHandler):
|
||||||
|
|
||||||
|
def __init__(self, encoding="utf-8", delimiter: str = ";"):
|
||||||
|
self.delimiter = delimiter
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def read_as_dict(self, filename: str, filedir: str = os.environ.get("PROJECT_DATA"), ) -> list[dict[str]]:
|
||||||
|
|
||||||
|
filename = f"{filename.removesuffix(".csv")}.csv"
|
||||||
|
filedir = self.check_filedir(filedir)
|
||||||
|
|
||||||
|
filepath = os.path.join(filedir, filename)
|
||||||
|
if not filepath:
|
||||||
|
raise FileNotFoundError("Zur angegebenen Teilenummer existiert keine Stücklisten-CSV!")
|
||||||
|
|
||||||
|
with open(filepath, "r", encoding=self.encoding, newline="") as csv_file:
|
||||||
|
reader = csv.DictReader(csv_file, delimiter=self.delimiter)
|
||||||
|
|
||||||
|
required_col_names = ["ParentCode", "Positionsnummer", "ChildCode", "Menge", "Uebergabelaenge"]
|
||||||
|
if reader.fieldnames != required_col_names:
|
||||||
|
raise ValueError(f"Spaltennamen falsch. Korrekt: {required_col_names}")
|
||||||
|
|
||||||
|
csv_data = []
|
||||||
|
for line in reader:
|
||||||
|
line["Positionsnummer"] += "0" # Angleichung Positionsnummer an SIVAS-Format (Pos. * 10)
|
||||||
|
csv_data.append(line)
|
||||||
|
return csv_data
|
||||||
|
|
||||||
|
def write_from_dict(self, filename: str, data: list[dict[str]], filedir: str = os.environ.get("PROJECT_WORK")) -> None:
|
||||||
|
|
||||||
|
filedir = self.check_filedir(filedir)
|
||||||
|
filepath = os.path.join(filedir, f"{filename.removesuffix(".csv")}.csv")
|
||||||
|
|
||||||
|
with open(filepath, "w", encoding=self.encoding, newline="") as csv_file:
|
||||||
|
writer = csv.DictWriter(csv_file,
|
||||||
|
fieldnames=data[0].keys(),
|
||||||
|
delimiter=self.delimiter)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(data)
|
||||||
|
|
||||||
|
self.check_filepath(filepath)
|
||||||
|
|
||||||
|
class SQLFileHandler(FileHandler):
|
||||||
|
|
||||||
|
def __init__(self, encoding: str = "utf-8"):
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def read_from_file(self, filename: str, filedir: str = os.environ.get("PROJECT_DATA")) -> str:
|
||||||
|
|
||||||
|
filedir = self.check_filedir(filedir)
|
||||||
|
|
||||||
|
filepath = os.path.join(filedir, filename)
|
||||||
|
filepath = self.check_filepath(filepath)
|
||||||
|
|
||||||
|
with open(filepath, "r", encoding="utf-8", newline="") as sql_file:
|
||||||
|
sql = sql_file.read()
|
||||||
|
return sql
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
if not os.path.isdir(os.path.dirname(db_path)):
|
||||||
|
raise NotADirectoryError
|
||||||
|
|
||||||
|
self.db_path = db_path
|
||||||
|
self.con = sqlite3.connect(self.db_path, autocommit=True)
|
||||||
|
self.con.row_factory = self.dict_factory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def dict_factory(cursor, row):
|
def dict_factory(cursor, row):
|
||||||
d = {}
|
d = {}
|
||||||
for idx, col in enumerate(cursor.description):
|
for idx, col in enumerate(cursor.description):
|
||||||
d[col[0]] = row[idx]
|
d[col[0]] = row[idx]
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def read_csv(filedir: str, partnumber: str) -> list[dict[str, str]]:
|
def close(self):
|
||||||
csv_path = os.path.join(filedir, f"BOM_{partnumber}.csv")
|
self.con.close()
|
||||||
if not csv_path:
|
|
||||||
raise FileNotFoundError
|
|
||||||
|
|
||||||
with open(csv_path, "r", encoding="utf-8", newline="") as csv_file:
|
def remove(self):
|
||||||
reader = csv.DictReader(csv_file, delimiter=";")
|
if os.path.isfile(self.db_path):
|
||||||
csv_data = []
|
os.remove(self.db_path)
|
||||||
for line in reader:
|
|
||||||
line["Positionsnummer"] += "0" # Angleichung Positionsnummer an SIVAS-Format (Pos. * 10)
|
|
||||||
csv_data.append(line)
|
|
||||||
|
|
||||||
return csv_data
|
|
||||||
|
|
||||||
def build_sivas_from_file(sql_filedir: str, sql_filename="sivas_replika.sql") -> None:
|
class RD_COMP:
|
||||||
filepath = os.path.join(sql_filedir, sql_filename)
|
|
||||||
|
|
||||||
if not filepath:
|
"""
|
||||||
raise FileNotFoundError
|
Temporäre Tabelle, in die die Daten der Stücklisten-CSV aus RuleDesigner geschrieben werden
|
||||||
|
Tabelle wird für die Verarbeitung der Stückliste benötigt. Sobald die finale Stückliste steht und in die Tabelle 'COMP'
|
||||||
|
wird sie nicht mehr benötigt.
|
||||||
|
"""
|
||||||
|
|
||||||
with open(filepath, "r", encoding="utf-8", newline="") as sql_file:
|
def __init__(self, db: Database, parent_code: str):
|
||||||
sql = sql_file.read()
|
self.db = db
|
||||||
|
self.parent_code = parent_code
|
||||||
|
|
||||||
con.executescript(sql)
|
def __repr__(self):
|
||||||
|
res = self.get_verbose_data()
|
||||||
|
output = self._sql_as_ascii_table(res)
|
||||||
|
return output
|
||||||
|
|
||||||
def get_new_bom_data(parent_partnum:str) -> list[dict[str, str]]:
|
def __str__(self):
|
||||||
""" Positionsnummern und zugehörige Teilenummern aus neuer Stückliste holen"""
|
res = self.get_compact_data()
|
||||||
res = con.execute("SELECT * FROM RD_COMP WHERE ParentCode = :parent ORDER BY Positionsnummer", {"parent": parent_partnum}).fetchall()
|
output = self._sql_as_ascii_table(res)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def create(self) -> None:
|
||||||
|
"""Schreibt neue Stücklistendaten aus CSV in temporäre Datenbanktabelle 'RD_COMP'."""
|
||||||
|
|
||||||
|
self.db.con.execute("""CREATE TABLE RD_COMP (ParentCode TEXT NOT NULL,
|
||||||
|
Positionsnummer INTEGER NOT NULL,
|
||||||
|
ChildCode TEXT NOT NULL,
|
||||||
|
Menge INTEGER NOT NULL,
|
||||||
|
Uebergabelaenge REAL NOT NULL,
|
||||||
|
PRIMARY KEY(Positionsnummer))""")
|
||||||
|
|
||||||
|
def drop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_csv_data(self, csv_data: list[dict[str]]):
|
||||||
|
self.db.con.executemany(""" INSERT INTO RD_COMP (ParentCode, Positionsnummer, ChildCode, Menge, Uebergabelaenge)
|
||||||
|
VALUES (:ParentCode, :Positionsnummer, :ChildCode, :Menge, :Uebergabelaenge)""", csv_data)
|
||||||
|
|
||||||
|
def get_all_bom_data(self) -> list[dict[str]]:
|
||||||
|
res = self.db.con.execute("SELECT * FROM RD_COMP WHERE ParentCode = :parent ORDER BY Positionsnummer",
|
||||||
|
{"parent": self.parent_code}).fetchall()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_old_bom_ids(parent_partnum: str) -> list | None:
|
def get_verbose_data(self) -> list[dict[str]]:
|
||||||
"""Gibt ID's der Datensätze aus der alten Stückliste zurück"""
|
res = self.db.con.execute("""
|
||||||
res = con.execute("SELECT ID FROM COMP WHERE PARENT = :parent", {"parent": parent_partnum}).fetchall()
|
SELECT C.*,
|
||||||
|
R.REGEL,
|
||||||
|
B.BEDINGUNG
|
||||||
|
FROM COMP C
|
||||||
|
LEFT JOIN VAR_TEIL_BEDINGUNG_2 B
|
||||||
|
ON C.ID = B.COMP_ID
|
||||||
|
LEFT JOIN STKL_REGEL R
|
||||||
|
ON C.ID = R.COMP_ID
|
||||||
|
WHERE C.PARENT = :parent_code
|
||||||
|
ORDER BY C.POSNR
|
||||||
|
""", {"parent_code": self.parent_code}).fetchall()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_positions_to_move(parent_partnum: str) -> set | None:
|
def get_compact_data(self) -> list[dict[str]]:
|
||||||
|
res = self.db.con.execute("""
|
||||||
|
SELECT C.POSNR,
|
||||||
|
C.KOMPTEILENR,
|
||||||
|
C.ABW_ANZAHL,
|
||||||
|
C.MENGE,
|
||||||
|
C.LAENGE,
|
||||||
|
C.ZUSCHNITT,
|
||||||
|
R.REGEL,
|
||||||
|
B.Bedingung
|
||||||
|
FROM COMP C
|
||||||
|
LEFT JOIN VAR_TEIL_BEDINGUNG_2 B
|
||||||
|
ON C.ID = B.COMP_ID
|
||||||
|
LEFT JOIN STKL_REGEL R
|
||||||
|
ON C.ID = R.COMP_ID
|
||||||
|
WHERE C.PARENT = :parent_code
|
||||||
|
ORDER BY C.POSNR
|
||||||
|
""", {"parent_code": self.parent_code}).fetchall()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _sql_as_ascii_table(self, sql_data: list[dict[str]]):
|
||||||
|
"""SQL-Rohdaten als Tabelle ausgeben"""
|
||||||
|
headers = list(sql_data[0].keys())
|
||||||
|
rows = [[str(row.get(h, "") if row.get(h, "") is not None else "") for h in headers] for row in sql_data]
|
||||||
|
col_widths = [max(len(str(h)), max(len(str(row[i])) for row in rows)) for i, h in enumerate(headers)]
|
||||||
|
header_line = " | ".join(f"{h:<{col_widths[i]}}" for i, h in enumerate(headers))
|
||||||
|
separator = "-+-".join("-" * col_widths[i] for i in range(len(headers)))
|
||||||
|
row_lines = [" | ".join(f"{row[i]:<{col_widths[i]}}" for i in range(len(headers))) for row in rows]
|
||||||
|
return "\n".join([header_line, separator] + row_lines)
|
||||||
|
|
||||||
|
def insert(self, new_data: dict[str, str]) -> None:
|
||||||
|
self.db.con.executemany("""INSERT INTO COMP (PARENT, POSNR, KOMPTEILENR, DISPO_AKTIV_JN, MENGE, ABW_ANZAHL, LAENGE, ZUSCHNITT, RUESTCODE)
|
||||||
|
VALUES (:PARENT, :POSNR, :KOMPTEILENR, :DISPO_AKTIV_JN, :MENGE, :ABW_ANZAHL, :LAENGE, :ZUSCHNITT, :RUESTCODE)""",
|
||||||
|
new_data)
|
||||||
|
|
||||||
|
def delete(self, ids: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("DELETE FROM COMP WHERE ID = :ID", ids)
|
||||||
|
|
||||||
|
def update(self, pos_data: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("""
|
||||||
|
UPDATE COMP
|
||||||
|
SET POSNR = :POSNR,
|
||||||
|
ABW_ANZAHL = :ABW_ANZAHL,
|
||||||
|
MENGE = :MENGE,
|
||||||
|
LAENGE = :LAENGE,
|
||||||
|
ZUSCHNITT = :ZUSCHNITT,
|
||||||
|
DISPO_AKTIV_JN = :DISPO_AKTIV_JN,
|
||||||
|
RUESTCODE = :RUESTCODE
|
||||||
|
WHERE ID = :ID
|
||||||
|
""", pos_data)
|
||||||
|
|
||||||
|
|
||||||
|
class COMP:
|
||||||
|
|
||||||
|
"""Beinhaltet Methoden, zur Interaktion mit Tabelle COMP (gleichnamig wie in SIVAS)"""
|
||||||
|
|
||||||
|
def __init__(self, db: Database):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def rebuild_sivas(self, sql: str) -> None:
|
||||||
|
self.db.con.executescript(sql)
|
||||||
|
|
||||||
|
def get_child_ids(self, parent_code: str) -> list[dict[str]]:
|
||||||
|
res = self.db.con.execute("SELECT ID FROM COMP WHERE PARENT = :parent",
|
||||||
|
{"parent": parent_code}).fetchall()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class VAR_TEIL_BEDINGUNGEN_2:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Beinhaltet Methoden, zur Interaktion mit Tabelle VAR_TEIL_BEDINGUNG_2 (gleichnamig wie in SIVAS),
|
||||||
|
in der TOS-Bedingungen gespeichert sind
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db: Database) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def update(self, data: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("UPDATE VAR_TEIL_BEDINGUNG_2 SET BEDINGUNG = :BEDINGUNG WHERE COMP_ID = :ID", data)
|
||||||
|
|
||||||
|
def delete(self, data: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("DELETE FROM VAR_TEIL_BEDINGUNG_2 WHERE COMP_ID = :ID", data)
|
||||||
|
|
||||||
|
|
||||||
|
class STKL_REGEL:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Beinhaltet Methoden, zur Interaktion mit Tabelle STKL_REGEL (gleichnamig wie in SIVAS),
|
||||||
|
in der TOS-Regeln gespeichert sind
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db: Database) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def update(self, data: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("UPDATE STKL_REGEL SET REGEL = :REGEL WHERE COMP_ID = :ID", data)
|
||||||
|
|
||||||
|
def delete(self, data: list[dict[str]]) -> None:
|
||||||
|
self.db.con.executemany("DELETE FROM STKL_REGEL WHERE COMP_ID = :ID", data)
|
||||||
|
|
||||||
|
class BOMProcessing:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Die Methoden dieser Klasse verarbeiten die Daten aus der RuleDesigner-Stücklisten CSV und initiiert das Schreiben der verarbeiteten
|
||||||
|
Daten in COMP.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db: Database, parent_code: str):
|
||||||
|
self.db = db
|
||||||
|
self.parent_code = parent_code
|
||||||
|
self.new_bom = RD_COMP(db, parent_code)
|
||||||
|
self.old_bom = COMP(db)
|
||||||
|
self.tos_bedingungen = VAR_TEIL_BEDINGUNGEN_2(db)
|
||||||
|
self.tos_regeln = STKL_REGEL(db)
|
||||||
|
|
||||||
|
def extract_col_data(self, query_results: list[dict[str]], *col_names: tuple) -> list[dict[str]]:
|
||||||
|
extracted_data = []
|
||||||
|
for result in query_results:
|
||||||
|
data = {col_name: result[col_name] for col_name in col_names if col_name in result}
|
||||||
|
extracted_data.append(data)
|
||||||
|
return extracted_data
|
||||||
|
|
||||||
|
def _get_positions_to_move_old_logic(self) -> set | None:
|
||||||
"""
|
"""
|
||||||
Gibt alle Positionsnummern zurück, die gemäß alter Logik verschoben werden
|
Gibt alle Positionsnummern zurück, die gemäß alter Logik verschoben werden
|
||||||
"""
|
"""
|
||||||
res = con.execute("""
|
res = self.db.con.execute("""
|
||||||
SELECT RD.Positionsnummer AS POS
|
SELECT RD.Positionsnummer AS POS
|
||||||
|
|
||||||
FROM RD_COMP RD
|
FROM RD_COMP RD
|
||||||
@@ -83,17 +366,17 @@ def get_positions_to_move(parent_partnum: str) -> set | None:
|
|||||||
AND RD.Uebergabelaenge = COALESCE(C.LAENGE,0)
|
AND RD.Uebergabelaenge = COALESCE(C.LAENGE,0)
|
||||||
|
|
||||||
WHERE RD.ParentCode = :parent
|
WHERE RD.ParentCode = :parent
|
||||||
""",
|
""", {"parent": self.parent_code}).fetchall()
|
||||||
{"parent": parent_partnum}).fetchall()
|
|
||||||
if res is not None:
|
if res is not None:
|
||||||
res = set([line["POS"] for line in res])
|
res = set([line["POS"] for line in res])
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def identical_occurences_in_both_boms(parent_partnum: str, child_id: str) -> bool:
|
def _check_identical_code_len_combo(self, child_code: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Prüfung Fall 1.1:
|
Prüfung Fall 1.1:
|
||||||
"""
|
"""
|
||||||
res = con.execute("""
|
res = self.db.con.execute("""
|
||||||
SELECT CASE
|
SELECT CASE
|
||||||
-- Prüfung 1: Liegen alle Teilenr.+Länge-Kombinationen aus der neuen Stückliste auch in der alten Stückliste vor?
|
-- Prüfung 1: Liegen alle Teilenr.+Länge-Kombinationen aus der neuen Stückliste auch in der alten Stückliste vor?
|
||||||
WHEN NOT EXISTS (
|
WHEN NOT EXISTS (
|
||||||
@@ -126,15 +409,15 @@ def identical_occurences_in_both_boms(parent_partnum: str, child_id: str) -> boo
|
|||||||
ELSE 'False'
|
ELSE 'False'
|
||||||
END AS 'Fall 1.1'
|
END AS 'Fall 1.1'
|
||||||
""",
|
""",
|
||||||
{"parent": parent_partnum, "child": child_id}).fetchone()
|
{"parent": self.parent_code, "child": child_code}).fetchone()
|
||||||
|
|
||||||
return True if res["Fall 1.1"] == 'True' else False
|
return True if res["Fall 1.1"] == 'True' else False
|
||||||
|
|
||||||
def occurences_at_same_pos_in_both_boms(parent_partnum: str, child_id: str) -> bool:
|
def _check_same_pos(self, child_code: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Prüfung Fall 1.2
|
Prüfung Fall 1.2
|
||||||
"""
|
"""
|
||||||
res = con.execute("""
|
res = self.db.con.execute("""
|
||||||
SELECT CASE
|
SELECT CASE
|
||||||
WHEN NOT EXISTS (
|
WHEN NOT EXISTS (
|
||||||
-- Prüfung, ob in der neuen Stückliste an denselben Positionen wie in der alten Stückliste Längenvarianten zur angegebenen Teilenr. vorliegen
|
-- Prüfung, ob in der neuen Stückliste an denselben Positionen wie in der alten Stückliste Längenvarianten zur angegebenen Teilenr. vorliegen
|
||||||
@@ -167,56 +450,15 @@ def occurences_at_same_pos_in_both_boms(parent_partnum: str, child_id: str) -> b
|
|||||||
ELSE 'False'
|
ELSE 'False'
|
||||||
END AS 'Fall 1.2'
|
END AS 'Fall 1.2'
|
||||||
""",
|
""",
|
||||||
{"parent": parent_partnum, "child": child_id}).fetchone()
|
{"parent": self.parent_code, "child": child_code}).fetchone()
|
||||||
return True if res["Fall 1.2"] == 'True' else False
|
return True if res["Fall 1.2"] == 'True' else False
|
||||||
|
|
||||||
def get_data_for_new_pos(parent_partnum: str, pos: str) -> list[dict[str, str]]:
|
def _get_update_data_for_diff_pos(self, pos_nr: str) -> list[dict[str]]:
|
||||||
"""
|
|
||||||
Fall 1.3 (Neuanlage von Positionen)
|
|
||||||
"""
|
|
||||||
res = con.execute("""
|
|
||||||
SELECT RD.Positionsnummer AS POSNR,
|
|
||||||
RD.ChildCode AS KOMPTEILENR,
|
|
||||||
RD.ParentCode AS PARENT,
|
|
||||||
CASE -- Abw. Anz. = NULL, wenn Pos. nicht längenabhängig, sonst Wert 'Menge' aus CSV
|
|
||||||
WHEN RD.Uebergabelaenge = 0 THEN NULL
|
|
||||||
ELSE RD.Menge
|
|
||||||
END AS ABW_ANZAHL,
|
|
||||||
CASE -- Mengenwert berechnen, wenn Pos. längenabhängig, sonst Wert 'Menge' aus CSV
|
|
||||||
WHEN RD.Uebergabelaenge = 0 THEN RD.Menge
|
|
||||||
ELSE RD.Menge * (RD.Uebergabelaenge + COALESCE(C.ZUSCHNITT,0)) / 1000
|
|
||||||
END AS MENGE,
|
|
||||||
CASE -- LAENGE = NULL, wenn Pos. nicht längenabhängig, sonst Wert 'Uebergabelaenge' aus CSV
|
|
||||||
WHEN RD.Uebergabelaenge = 0 THEN NULL
|
|
||||||
ELSE RD.Uebergabelaenge
|
|
||||||
END AS LAENGE,
|
|
||||||
NULL AS ZUSCHNITT,
|
|
||||||
P.DISPO_AKTIV_JN,
|
|
||||||
P.RUESTCODE
|
|
||||||
|
|
||||||
FROM RD_COMP RD
|
|
||||||
LEFT JOIN COMP C
|
|
||||||
ON RD.ChildCode = C.KOMPTEILENR
|
|
||||||
AND RD.Uebergabelaenge = COALESCE(C.LAENGE, 0)
|
|
||||||
AND RD.ParentCode = C.PARENT
|
|
||||||
|
|
||||||
-- Da die Positionen neu angelegt werden, werden die Defaultwerte verwendet, die zum Teil aus dem Teilestamm (PARTS) kommen.
|
|
||||||
LEFT JOIN PARTS P
|
|
||||||
ON RD.ChildCode = P.TEILENR
|
|
||||||
|
|
||||||
WHERE C.KOMPTEILENR IS NULL
|
|
||||||
AND RD.ParentCode = :parent
|
|
||||||
AND RD.Positionsnummer = :posnr
|
|
||||||
""",
|
|
||||||
{"parent": parent_partnum, "posnr": pos}).fetchone()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_update_data_for_diff_pos(parent_partnum: str, pos: str) -> list[dict[str, str]]:
|
|
||||||
"""
|
"""
|
||||||
Ermittelt die Daten, die an die neue Position verschoben werden sollen.
|
Ermittelt die Daten, die an die neue Position verschoben werden sollen.
|
||||||
(Für Fall 1.1 und Fall 1.3)
|
(Für Fall 1.1 und Fall 1.3)
|
||||||
"""
|
"""
|
||||||
res = con.execute("""
|
res = self.db.con.execute("""
|
||||||
SELECT C.ID,
|
SELECT C.ID,
|
||||||
C.DISPO_AKTIV_JN,
|
C.DISPO_AKTIV_JN,
|
||||||
C.ZUSCHNITT,
|
C.ZUSCHNITT,
|
||||||
@@ -258,15 +500,15 @@ def get_update_data_for_diff_pos(parent_partnum: str, pos: str) -> list[dict[str
|
|||||||
WHERE RD.ParentCode = :parent
|
WHERE RD.ParentCode = :parent
|
||||||
AND RD.Positionsnummer = :posnr
|
AND RD.Positionsnummer = :posnr
|
||||||
""",
|
""",
|
||||||
{"parent": parent_partnum, "posnr": pos}).fetchone()
|
{"parent": self.parent_code, "posnr": pos_nr}).fetchone()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_update_data_for_same_pos(parent_partnum: str, pos: str) -> list[dict[str, str]]:
|
def _get_update_data_for_same_pos(self, pos_nr: str) -> list[dict[str]]:
|
||||||
"""
|
"""
|
||||||
Ermittelt die Daten, die an dieselbe Position verschoben werden sollen
|
Ermittelt die Daten, die an dieselbe Position verschoben werden sollen
|
||||||
(Für Fall 1.2)
|
(Für Fall 1.2)
|
||||||
"""
|
"""
|
||||||
res = con.execute("""
|
res = self.db.con.execute("""
|
||||||
SELECT C.ID,
|
SELECT C.ID,
|
||||||
C.ZUSCHNITT,
|
C.ZUSCHNITT,
|
||||||
C.DISPO_AKTIV_JN,
|
C.DISPO_AKTIV_JN,
|
||||||
@@ -308,202 +550,146 @@ def get_update_data_for_same_pos(parent_partnum: str, pos: str) -> list[dict[str
|
|||||||
WHERE RD.ParentCode = :parent
|
WHERE RD.ParentCode = :parent
|
||||||
AND RD.Positionsnummer = :posnr
|
AND RD.Positionsnummer = :posnr
|
||||||
""",
|
""",
|
||||||
{"parent": parent_partnum, "posnr": pos}).fetchone()
|
{"parent": self.parent_code, "posnr": pos_nr}).fetchone()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def create_new_bom(parent_partnum:str) -> None:
|
def _get_ids_to_del(self, ignored_ids: list = []) -> list[dict[str]]:
|
||||||
|
"""
|
||||||
|
Die zu löschenden ID's ergeben sich, indem man die ID's der Positionen aus der alten Stückliste betrachtet
|
||||||
|
und die ID's aus der neuen Stückliste abzieht, die verschoben werden. Positionen, die in der neuen Stückliste
|
||||||
|
neu dazukommen sind, wirken sich nicht auf die zu löschenden ID's aus!
|
||||||
|
"""
|
||||||
|
old_bom_ids = self.old_bom.get_child_ids(self.parent_code)
|
||||||
|
ids_to_delete = [idx for idx in old_bom_ids if idx["ID"] not in ignored_ids]
|
||||||
|
return ids_to_delete
|
||||||
|
|
||||||
new_bom_data = get_new_bom_data(parent_partnum)
|
def _get_data_for_insert(self, posnr: str) -> list[dict[str]]:
|
||||||
|
"""
|
||||||
|
Fall 1.3 (Neuanlage von Positionen)
|
||||||
|
"""
|
||||||
|
res = self.db.con.execute("""
|
||||||
|
SELECT RD.Positionsnummer AS POSNR,
|
||||||
|
RD.ChildCode AS KOMPTEILENR,
|
||||||
|
RD.ParentCode AS PARENT,
|
||||||
|
NULL AS ZUSCHNITT,
|
||||||
|
P.DISPO_AKTIV_JN,
|
||||||
|
P.RUESTCODE,
|
||||||
|
|
||||||
# Ermittlung, welche Teilenummern unter Fall 1.1 , 1.2 oder 1.3 fallen
|
-- Bestimmt Wert für Spalte 'ABW_ANZAHL': NULL, wenn Pos. nicht längenabhängig, sonst Wert 'Menge' aus CSV
|
||||||
# Nicht im folgenden Loop, um mehrfache Fallprüfung 1.1 und 1.2 zu vermeiden (wenn längenabhängige Teilenummern mehrfach in Stückliste vorliegen)
|
CASE
|
||||||
partnum_cases = {}
|
WHEN RD.Uebergabelaenge = 0 THEN NULL
|
||||||
for result_line in new_bom_data:
|
ELSE RD.Menge
|
||||||
child_partnum = result_line["ChildCode"]
|
END AS ABW_ANZAHL,
|
||||||
if child_partnum not in partnum_cases:
|
|
||||||
if identical_occurences_in_both_boms(parent_partnum, child_partnum):
|
|
||||||
partnum_cases[child_partnum] = 1.1
|
|
||||||
elif occurences_at_same_pos_in_both_boms(parent_partnum, child_partnum):
|
|
||||||
partnum_cases[child_partnum] = 1.2
|
|
||||||
else:
|
|
||||||
partnum_cases[child_partnum] = 1.3
|
|
||||||
|
|
||||||
# Ermittlung unter welchen Fall der Logik, die jeweilige Positionsnummer der neuen Stückliste fällt
|
-- Bestimmt Wert für Spalte 'MENGE': Berechnung Dezimalwert, wenn Pos. längenabhängig, sonst Wert 'Menge' aus CSV
|
||||||
pos_to_update = [] # Sammelt Positionsnummern (inkl. Daten), die upgedated werden sollen
|
CASE
|
||||||
pos_to_insert = [] # Sammelt Positionsnummern (inkl. Daten), die eingefügt werden sollen
|
WHEN RD.Uebergabelaenge = 0 THEN RD.Menge
|
||||||
pos_to_del = [] # Sammelt Positionsnummern, die gelöscht werden sollen
|
ELSE RD.Menge * (RD.Uebergabelaenge + COALESCE(C.ZUSCHNITT,0)) / 1000
|
||||||
moved_partnums = [] # Sammelt Positionsnummern, die verschoben werden. Dient später zur Ermittlung der zu löschenden Positionen
|
END AS MENGE,
|
||||||
|
|
||||||
|
-- Bestimmt Wert für Spalte 'LAENGE': NULL, wenn Pos. nicht längenabhängig, sonst Wert 'Uebergabelaenge' aus CSV
|
||||||
|
CASE
|
||||||
|
WHEN RD.Uebergabelaenge = 0 THEN NULL
|
||||||
|
ELSE RD.Uebergabelaenge
|
||||||
|
END AS LAENGE
|
||||||
|
|
||||||
|
FROM RD_COMP RD
|
||||||
|
LEFT JOIN COMP C
|
||||||
|
ON RD.ChildCode = C.KOMPTEILENR
|
||||||
|
AND RD.Uebergabelaenge = COALESCE(C.LAENGE, 0)
|
||||||
|
AND RD.ParentCode = C.PARENT
|
||||||
|
|
||||||
|
-- Da es sich um neue Positionen handelt, werden Defaultwerte verwendet, die z.T. aus dem Teilestamm (PARTS) kommen
|
||||||
|
LEFT JOIN PARTS P
|
||||||
|
ON RD.ChildCode = P.TEILENR
|
||||||
|
|
||||||
|
WHERE C.KOMPTEILENR IS NULL
|
||||||
|
AND RD.ParentCode = :parent
|
||||||
|
AND RD.Positionsnummer = :posnr
|
||||||
|
""",
|
||||||
|
{"parent": self.parent_code, "posnr": posnr}).fetchone()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def write_new_bom(self) -> None:
|
||||||
|
"""
|
||||||
|
Diese Methode binhaltet die Hauptlogik:
|
||||||
|
Hier wird festgelegt, welche Positionsnummern eingefügt, gelöscht oder verschoben werden müssen
|
||||||
|
Am Ende werden die daraus erforderlichen Datenbankaktionen (insert / update / delete) initiiert,
|
||||||
|
um die neue, fertig verarbeite Stückliste in COMP zu überführen.
|
||||||
|
"""
|
||||||
|
update_data_comp = []
|
||||||
|
insert_data_comp = []
|
||||||
|
update_data_regel = []
|
||||||
|
update_data_bedingung = []
|
||||||
|
ids_to_delete = []
|
||||||
|
ids_not_to_delete = []
|
||||||
|
childcodes_case_11 = set() # vermeidet erneute Prüfung einer Teilenummer durch _check_identical_code_len_combo
|
||||||
|
childcodes_case_12 = set() # vermeidet erneute Prüfung einer Teilenummer durch _check_same_pos()
|
||||||
|
|
||||||
|
new_bom_data = self.new_bom.get_all_bom_data()
|
||||||
|
|
||||||
for pos_data in new_bom_data:
|
for pos_data in new_bom_data:
|
||||||
child_partnum = pos_data["ChildCode"]
|
|
||||||
child_pos = pos_data["Positionsnummer"]
|
|
||||||
|
|
||||||
# Fall 1.1 (Kann nicht mit 1. Fall in Fall 1.3.1 (siehe unten) fusioniert werden, weil sonst z.B. bei Beispiel 3 fälschlicherweise diese Logik greifen würde)
|
child_code: str = pos_data["ChildCode"]
|
||||||
if partnum_cases[child_partnum] == 1.1:
|
posnr: int = pos_data["Positionsnummer"]
|
||||||
data = get_update_data_for_diff_pos(parent_partnum, child_pos)
|
|
||||||
pos_to_update.append(data)
|
|
||||||
moved_partnums.append(data["ID"])
|
|
||||||
|
|
||||||
# Fall 1.2
|
# Prüfung Fall 1.1
|
||||||
elif partnum_cases[child_partnum] == 1.2:
|
if child_code in childcodes_case_11 or self._check_identical_code_len_combo(child_code) is True:
|
||||||
data = get_update_data_for_same_pos(parent_partnum, child_pos)
|
logic_case = 1.1
|
||||||
pos_to_update.append(data)
|
childcodes_case_11.add(child_code)
|
||||||
moved_partnums.append(data["ID"])
|
data = self._get_update_data_for_diff_pos(posnr)
|
||||||
|
|
||||||
# Fall 1.3
|
# Prüfung Fall 1.2
|
||||||
elif partnum_cases[child_partnum] == 1.3:
|
elif child_code in childcodes_case_12 or self._check_same_pos(child_code) is True:
|
||||||
|
logic_case = 1.2
|
||||||
|
childcodes_case_12.add(child_code)
|
||||||
|
data = self._get_update_data_for_same_pos(posnr)
|
||||||
|
|
||||||
# Fall 1.3.1 - Verschiebung von Positionen
|
# Prüfungen Fall 1.3
|
||||||
pos_to_move = get_positions_to_move(parent_partnum)
|
|
||||||
if child_pos in pos_to_move:
|
|
||||||
data = get_update_data_for_diff_pos(parent_partnum, child_pos)
|
|
||||||
pos_to_update.append(data)
|
|
||||||
moved_partnums.append(data["ID"])
|
|
||||||
|
|
||||||
# Fall 1.3.2 - Neuanlage von Positionen
|
|
||||||
else:
|
else:
|
||||||
data = get_data_for_new_pos(parent_partnum, child_pos)
|
|
||||||
pos_to_insert.append(data)
|
# Prüfung Fall 1.31 - Verschiebung bestehender Positionen
|
||||||
|
posnrs_to_move = self._get_positions_to_move_old_logic()
|
||||||
|
if posnr in posnrs_to_move:
|
||||||
|
logic_case = 1.31
|
||||||
|
data = self._get_update_data_for_diff_pos(posnr)
|
||||||
|
|
||||||
|
# Prüfung Fall 1.32 - Einfügen neuer Positionen
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Fall ist nicht vorgesehen (dürfte laut spezifizierter Logik nicht auftreten). Prüfung erforderlich!")
|
logic_case = 1.32
|
||||||
|
data = self._get_data_for_insert(posnr)
|
||||||
|
|
||||||
# Positionen aus der alten Stückliste, die nicht verschoben werden müssen gelöscht werden.
|
# Teilenummern der Stückliste, die verschoben werden (Fal 1.1 / 1.2 / 1.3.1):
|
||||||
# Positionen, die in der neuen Stückliste neu eingefügt werden wirken sich nicht auf die Positionen der alten Stückliste aus und bleiben
|
# - werden nie gelöscht (konkret: deren ID's in COMP), weil IMMER eine Verschiebung der Positionsnummer erfolgt
|
||||||
# deshalb bei der Bestimmung der zu löschenden Positionen unberücksichtigt.
|
if logic_case in (1.1, 1.2, 1.31):
|
||||||
old_bom_ids = get_old_bom_ids(parent_partnum)
|
ids_not_to_delete.append(data["ID"])
|
||||||
for idx in old_bom_ids:
|
update_data_comp.append(data)
|
||||||
if idx["ID"] not in moved_partnums:
|
|
||||||
pos_to_del.append(idx)
|
|
||||||
|
|
||||||
# Datenbankaktionen durchführen, um die neue Stückliste zu schreiben.
|
if logic_case == 1.32:
|
||||||
if pos_to_del:
|
insert_data_comp.append(data)
|
||||||
delete_pos(pos_to_del)
|
|
||||||
if pos_to_insert:
|
|
||||||
insert_pos(pos_to_insert)
|
|
||||||
if pos_to_update:
|
|
||||||
update_pos(pos_to_update)
|
|
||||||
|
|
||||||
def insert_pos(new_data: dict[str, str]) -> None:
|
|
||||||
"""
|
|
||||||
Fügt die Positionen in die Stücklist ein, bei denen keine Verschiebung von der alten Stückliste erfolgt ist.
|
|
||||||
Diese Positionen werden stets ohne TOS-Regeln oder -Bedingungen angelegt.
|
|
||||||
"""
|
|
||||||
con.executemany("""INSERT INTO COMP (PARENT, POSNR, KOMPTEILENR, DISPO_AKTIV_JN, MENGE, ABW_ANZAHL, LAENGE, ZUSCHNITT, RUESTCODE)
|
|
||||||
VALUES (:PARENT, :POSNR, :KOMPTEILENR, :DISPO_AKTIV_JN, :MENGE, :ABW_ANZAHL, :LAENGE, :ZUSCHNITT, :RUESTCODE)""",
|
|
||||||
new_data)
|
|
||||||
|
|
||||||
def delete_pos(ids: list[dict[str, str]]) -> None:
|
|
||||||
"""
|
|
||||||
- Entfernt die Positionen aus der Stückliste (COMP), die nicht verschoben werden und nicht mehr in der alten Stückliste vorliegen.
|
|
||||||
- Entfernt die TOS-Regeln aus STKL_REGEL und TOS-Bedingungen aus VAR_TEIL_BEDINGUNG_2 zu den oben genannten Positionen.
|
|
||||||
"""
|
|
||||||
con.executemany("DELETE FROM COMP WHERE ID = :ID", ids)
|
|
||||||
con.executemany("DELETE FROM VAR_TEIL_BEDINGUNG_2 WHERE COMP_ID = :ID", ids)
|
|
||||||
con.executemany("DELETE FROM STKL_REGEL WHERE COMP_ID = :ID", ids)
|
|
||||||
|
|
||||||
def update_pos(new_data: list[dict[str, str]]) -> None:
|
|
||||||
"""
|
|
||||||
- Verschiebt Positionsdaten von einer Position in der alten Stückliste zu einer Position in der neuen Stückliste
|
|
||||||
- Aktualisiert TOS-Regeln in STKL_REGEL und
|
|
||||||
"""
|
|
||||||
con.executemany("""
|
|
||||||
UPDATE COMP
|
|
||||||
SET POSNR = :POSNR,
|
|
||||||
ABW_ANZAHL = :ABW_ANZAHL,
|
|
||||||
MENGE = :MENGE,
|
|
||||||
LAENGE = :LAENGE,
|
|
||||||
ZUSCHNITT = :ZUSCHNITT,
|
|
||||||
DISPO_AKTIV_JN = :DISPO_AKTIV_JN,
|
|
||||||
RUESTCODE = :RUESTCODE
|
|
||||||
WHERE ID = :ID
|
|
||||||
""", new_data)
|
|
||||||
|
|
||||||
update_bedingungen = [pos_data for pos_data in new_data if pos_data["BEDINGUNG"] is not None]
|
|
||||||
update_regeln = [pos_data for pos_data in new_data if pos_data["REGEL"] is not None]
|
|
||||||
|
|
||||||
if update_bedingungen:
|
|
||||||
con.executemany("UPDATE VAR_TEIL_BEDINGUNG_2 SET BEDINGUNG = :BEDINGUNG WHERE COMP_ID = :ID", update_bedingungen)
|
|
||||||
if update_regeln:
|
|
||||||
con.executemany("UPDATE STKL_REGEL SET REGEL = :REGEL WHERE COMP_ID = :ID", update_regeln)
|
|
||||||
|
|
||||||
def print_new_bom(parent_partnum: str) -> None:
|
|
||||||
res = con.execute("""
|
|
||||||
SELECT C.POSNR AS Pos,
|
|
||||||
C.KOMPTEILENR AS 'Art.-Nr.',
|
|
||||||
C.MENGE As Menge,
|
|
||||||
C.ABW_ANZAHL AS 'Abw. Anzahl',
|
|
||||||
C.LAENGE AS Länge,
|
|
||||||
C.ZUSCHNITT AS Zuschnitt,
|
|
||||||
B.BEDINGUNG AS Bedingung,
|
|
||||||
R.REGEL AS Regel,
|
|
||||||
BSP.Beispielnr AS Beispiel
|
|
||||||
|
|
||||||
FROM COMP C
|
|
||||||
LEFT JOIN VAR_TEIL_BEDINGUNG_2 B
|
|
||||||
ON C.ID = B.COMP_ID
|
|
||||||
|
|
||||||
LEFT JOIN STKL_REGEL R
|
|
||||||
ON C.ID = R.COMP_ID
|
|
||||||
|
|
||||||
LEFT JOIN Beispiele BSP
|
|
||||||
ON C.PARENT = BSP.Parent_ID
|
|
||||||
|
|
||||||
WHERE C.PARENT = :parent
|
|
||||||
ORDER BY POSNR
|
|
||||||
""",
|
|
||||||
{"parent": parent_partnum}).fetchall()
|
|
||||||
|
|
||||||
for line in res:
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
def write_bom_as_csv(parent_partnum: str) -> None:
|
|
||||||
out_path = f".data/BOM_{parent_partnum}_new.csv"
|
|
||||||
|
|
||||||
res: list[dict[str, str]]
|
|
||||||
res = con.execute("""
|
|
||||||
SELECT C.POSNR AS Pos,
|
|
||||||
C.KOMPTEILENR AS 'Art.-Nr.',
|
|
||||||
C.MENGE As Menge,
|
|
||||||
C.ABW_ANZAHL AS 'Abw. Anzahl',
|
|
||||||
C.LAENGE AS Länge,
|
|
||||||
C.ZUSCHNITT AS Zuschnitt,
|
|
||||||
B.BEDINGUNG AS Bedingung,
|
|
||||||
R.REGEL AS Regel,
|
|
||||||
BSP.Beispielnr AS Beispiel
|
|
||||||
|
|
||||||
FROM COMP C
|
|
||||||
LEFT JOIN VAR_TEIL_BEDINGUNG_2 B
|
|
||||||
ON C.ID = B.COMP_ID
|
|
||||||
|
|
||||||
LEFT JOIN STKL_REGEL R
|
|
||||||
ON C.ID = R.COMP_ID
|
|
||||||
|
|
||||||
LEFT JOIN Beispiele BSP
|
|
||||||
ON C.PARENT = BSP.Parent_ID
|
|
||||||
|
|
||||||
WHERE C.PARENT = :parent
|
|
||||||
ORDER BY POSNR
|
|
||||||
""",
|
|
||||||
{"parent": args["partnumber"]}).fetchall()
|
|
||||||
|
|
||||||
with open("test.csv", "w", encoding="utf-8", newline="") as csv_file:
|
|
||||||
writer = csv.DictWriter(csv_file, delimiter=";", fieldnames=res[0].keys())
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(res)
|
|
||||||
|
|
||||||
|
|
||||||
|
# Zu löschende Positionen ermitteln
|
||||||
|
ids_to_delete = self._get_ids_to_del(ignored_ids=ids_not_to_delete)
|
||||||
|
|
||||||
|
# Positionen löschen (falls erforderlich)
|
||||||
|
if ids_to_delete:
|
||||||
|
self.new_bom.delete(ids_to_delete)
|
||||||
|
self.tos_bedingungen.delete(ids_to_delete)
|
||||||
|
self.tos_regeln.delete(ids_to_delete)
|
||||||
|
|
||||||
|
# Positionen einfügen (falls erforderlich). Neue Positionen haben nie TOS-Regeln oder -Bedingungen
|
||||||
|
if insert_data_comp:
|
||||||
|
self.new_bom.insert(insert_data_comp)
|
||||||
|
|
||||||
|
# Positionen sowie deren TOS-Regeln und -Bedingungen updaten (falls erforderlich)
|
||||||
|
if update_data_comp:
|
||||||
|
update_data_regel = self.extract_col_data(update_data_comp, "ID", "REGEL")
|
||||||
|
update_data_bedingung = self.extract_col_data(update_data_comp, "ID", "BEDINGUNG")
|
||||||
|
|
||||||
|
self.new_bom.update(update_data_comp)
|
||||||
|
self.tos_regeln.update(update_data_regel)
|
||||||
|
self.tos_bedingungen.update(update_data_bedingung)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
main()
|
||||||
parser.add_argument("-cd", "--csv_dir", help="Verzeichnis, in der CSV-Datei mit den neuen Stücklisteninformationen liegt", default=r".\data")
|
|
||||||
parser.add_argument("-sd", "--sql_dir", help="Verzeichnis, in der SQL-Datei mit den Informationen zur Teilreplizierung der SIVAS-Datenbank liegt", default=r".\data")
|
|
||||||
parser.add_argument("-n", "--partnumber", help="Teilenummer der Baugruppe", type=str, default="000000382")
|
|
||||||
args: dict = vars(parser.parse_args())
|
|
||||||
|
|
||||||
# Vorerst wird echte bei jedem Lauf neue db-Datei erstellt, da es mit Datenbank im RAM (:memory:) teilweise zu fehlerhaften Abfragen kam
|
|
||||||
if "sivas_replika.db" in os.listdir("."):
|
|
||||||
os.remove("./sivas_replika.db")
|
|
||||||
|
|
||||||
with closing(sqlite3.connect("./sivas_replika.db", autocommit=True)) as con:
|
|
||||||
con.row_factory = dict_factory
|
|
||||||
main(args["partnumber"], args["sql_dir"], args["csv_dir"])
|
|
||||||
Reference in New Issue
Block a user