Nummernkreise für alle Produkte aus das Produktdatenblatt extrahiert. Fortna als Beispiel .xlsx unter examples dazu. create_examplex.py so impl., dass aus dem Sivas Export eine Liste der wirklich verbauten Produkte extrahiert werden kann, wenn man die cfg Produktnummern richtig pflegt

This commit is contained in:
2026-04-30 17:14:16 +02:00
parent 1f9193d332
commit a7bc331a7f
5 changed files with 1773 additions and 41 deletions
+276 -13
View File
@@ -8,8 +8,12 @@ Verwendung:
from __future__ import annotations
import argparse
import csv
import json
import logging
import re
import sys
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -21,6 +25,7 @@ log = logging.getLogger(__name__)
class StkItem:
position: str
teilenummer: str
bezeichnung: str
menge: Any
@@ -47,7 +52,7 @@ def _extract_stk_liste_ist(xlsx_path: Path) -> list[StkItem]:
ws = wb["Stk.-Liste IST"]
header_idx: tuple[int | None, int | None, int | None] | None = None
header_idx: tuple[int | None, int | None, int | None, int | None] | None = None
items: list[StkItem] = []
for row in ws.iter_rows(values_only=True):
@@ -57,30 +62,230 @@ def _extract_stk_liste_ist(xlsx_path: Path) -> list[StkItem]:
list(row),
["teilenummer", "teilnummer", "article", "artikelnummer"],
)
desc_idx = _find_col_index(
list(row),
["bezeichnung", "beschreibung", "description", "benennung"],
)
qty_idx = _find_col_index(list(row), ["menge", "anzahl", "qty", "quantity"])
if pos_idx is not None and part_idx is not None and qty_idx is not None:
header_idx = (pos_idx, part_idx, qty_idx)
if (
pos_idx is not None
and part_idx is not None
and desc_idx is not None
and qty_idx is not None
):
header_idx = (pos_idx, part_idx, desc_idx, qty_idx)
continue
pos_idx, part_idx, qty_idx = header_idx
assert pos_idx is not None and part_idx is not None and qty_idx is not None
pos_idx, part_idx, desc_idx, qty_idx = header_idx
assert (
pos_idx is not None
and part_idx is not None
and desc_idx is not None
and qty_idx is not None
)
position = _cell_str(row[pos_idx]) if pos_idx < len(row) else ""
teilenummer = _cell_str(row[part_idx]) if part_idx < len(row) else ""
bezeichnung = _cell_str(row[desc_idx]) if desc_idx < len(row) else ""
menge = row[qty_idx] if qty_idx < len(row) else None
# Leere Zeilen ueberspringen
if not position and not teilenummer and menge in ("", None):
if not position and not teilenummer and not bezeichnung and menge in ("", None):
continue
if not position or not teilenummer:
continue
items.append(StkItem(position=position, teilenummer=teilenummer, menge=menge))
items.append(
StkItem(
position=position,
teilenummer=teilenummer,
bezeichnung=bezeichnung,
menge=menge,
)
)
wb.close()
return items
def _process_single_folder(folder: Path, example_dir: Path, result: dict, log) -> None:
def _to_number(value: Any) -> float:
if value is None:
return 0.0
if isinstance(value, (int, float)):
return float(value)
text = str(value).strip().replace(",", ".")
if not text:
return 0.0
try:
return float(text)
except ValueError:
return 0.0
def _common_path_prefix(paths: list[str]) -> str:
parts_list = [p.split("/") for p in paths if p]
if not parts_list:
return ""
common = parts_list[0]
for parts in parts_list[1:]:
i = 0
max_i = min(len(common), len(parts))
while i < max_i and common[i] == parts[i]:
i += 1
common = common[:i]
if not common:
break
return "/".join(common)
def _load_product_numbers(cfg_path: Path) -> dict[str, str]:
"""
Laedt 9-stellige Produktnummern aus cfg/product_numbers.cfg.
Erwartetes Format:
[Sektion]
123456789
987654321
"""
allowed_sections = {"omniflo", "ils", "cpc", "trolley", "geruest"}
products: dict[str, str] = {}
current_section = ""
if not cfg_path.exists():
return products
for raw_line in cfg_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
current_section = line[1:-1].strip().lower()
continue
if current_section not in allowed_sections:
continue
if re.match(r"^\d{9}$", line):
products[line] = current_section
return products
def write_results(
xlsx_path: Path,
filtered_items: list[dict[str, Any]],
log: logging.Logger,
write_json: bool = False,
write_csv: bool = False,
write_xlsx: bool = False,
) -> None:
if write_json:
json_path = xlsx_path.with_suffix(".json")
with json_path.open("w", encoding="utf-8") as fh:
json.dump(filtered_items, fh, indent=2, ensure_ascii=False, default=str)
log.info("JSON geschrieben: %s", json_path)
if write_csv:
csv_path = xlsx_path.with_suffix(".csv")
with csv_path.open("w", encoding="utf-8-sig", newline="") as fh:
writer = csv.writer(fh, delimiter=";")
writer.writerow(["position", "teilenummer", "bezeichnung", "menge"])
for row in filtered_items:
writer.writerow(
[
row.get("position", ""),
row.get("teilenummer", ""),
row.get("bezeichnung", ""),
row.get("menge", ""),
]
)
log.info("CSV geschrieben: %s", csv_path)
if write_xlsx:
import openpyxl
xlsx_out = xlsx_path.with_name(f"{xlsx_path.stem}_result.xlsx")
wb = openpyxl.Workbook()
ws_3 = wb.active
ws_3.title = "3er"
ws_4 = wb.create_sheet("4er")
ws_result = wb.create_sheet("Result")
ws_product = wb.create_sheet("product")
headers = ["position", "teilenummer", "bezeichnung", "menge"]
ws_3.append(headers)
ws_4.append(headers)
aggregated: dict[str, dict[str, Any]] = defaultdict(
lambda: {
"teilenummer": "",
"bezeichnung": "",
"gesamtmenge": 0.0,
"positions": [],
}
)
for row in filtered_items:
slash_count = str(row.get("position", "")).count("/")
if slash_count == 3:
ws_3.append([row.get(h, "") for h in headers])
elif slash_count == 4:
ws_4.append([row.get(h, "") for h in headers])
tn = str(row.get("teilenummer", "")).strip()
if not tn:
continue
if not aggregated[tn]["teilenummer"]:
aggregated[tn]["teilenummer"] = tn
aggregated[tn]["bezeichnung"] = row.get("bezeichnung", "")
aggregated[tn]["gesamtmenge"] += _to_number(row.get("menge"))
aggregated[tn]["positions"].append(str(row.get("position", "")).strip())
ws_result.append(["teilenummer", "bezeichnung", "gesamtmenge", "mainpath", "endpath"])
project_root = Path(__file__).resolve().parent.parent
product_cfg = project_root / "cfg" / "product_numbers.cfg"
product_numbers = _load_product_numbers(product_cfg)
ws_product.append(["teilenummer", "bezeichnung", "gesamtmenge", "mainpath", "endpath"])
for tn in sorted(aggregated.keys()):
positions = [p for p in aggregated[tn]["positions"] if p]
mainpath = _common_path_prefix(positions)
variable_parts: list[str] = []
for pos in positions:
if mainpath:
suffix = pos[len(mainpath):].lstrip("/")
else:
suffix = pos
if suffix and suffix not in variable_parts:
variable_parts.append(suffix)
endpath = ",".join(variable_parts)
result_row = [
aggregated[tn]["teilenummer"],
aggregated[tn]["bezeichnung"],
aggregated[tn]["gesamtmenge"],
mainpath,
endpath,
]
ws_result.append(result_row)
if tn in product_numbers:
ws_product.append(result_row)
wb.save(str(xlsx_out))
wb.close()
log.info("XLSX geschrieben: %s", xlsx_out)
def _process_single_folder(
folder: Path,
example_dir: Path,
result: dict[str, list[dict[str, Any]]],
log: logging.Logger,
write_json: bool = False,
write_csv: bool = False,
write_xlsx: bool = False,
) -> None:
xlsx_files = sorted(folder.glob("*.xlsx"))
if not xlsx_files:
return
@@ -92,17 +297,34 @@ def _process_single_folder(folder: Path, example_dir: Path, result: dict, log) -
except ValueError as exc:
log.warning("%s/%s uebersprungen: %s", rel_folder, xlsx_path.name, exc)
return
size_path = (3, 4)
result[rel_folder] = [
filtered_items = [
{
"position": item.position,
"teilenummer": item.teilenummer,
"bezeichnung": item.bezeichnung,
"menge": item.menge,
}
for item in items
if item.position.count("/") in size_path
]
result[rel_folder] = filtered_items
write_results(
xlsx_path,
filtered_items,
log,
write_json=write_json,
write_csv=write_csv,
write_xlsx=write_xlsx,
)
def process_all(example_dir: Path) -> dict[str, list[dict[str, Any]]]:
def process_all(
example_dir: Path,
write_json: bool = False,
write_csv: bool = False,
write_xlsx: bool = False,
) -> dict[str, list[dict[str, Any]]]:
"""
Durchsucht rekursiv alle Unterverzeichnisse in example_dir.
@@ -123,7 +345,15 @@ def process_all(example_dir: Path) -> dict[str, list[dict[str, Any]]]:
result: dict[str, list[dict[str, Any]]] = {}
for folder in subdirs:
log.info("Verarbeite %s", folder)
_process_single_folder(folder, example_dir, result, log)
_process_single_folder(
folder,
example_dir,
result,
log,
write_json=write_json,
write_csv=write_csv,
write_xlsx=write_xlsx,
)
log.info("Fertig. Gelesene Verzeichnisse mit Daten: %d", len(result))
return result
@@ -144,6 +374,24 @@ def main() -> None:
)
parser.add_argument("--verbose", "-v", action="store_true",
help="Ausfuehrliche Ausgabe")
parser.add_argument(
"--2json",
dest="to_json",
action="store_true",
help="Schreibt Ausgabe als .json neben die jeweilige .xlsx",
)
parser.add_argument(
"--2csv",
dest="to_csv",
action="store_true",
help="Schreibt Ausgabe als .csv neben die jeweilige .xlsx",
)
parser.add_argument(
"--2xlsx",
dest="to_xlsx",
action="store_true",
help="Schreibt Ausgabe als _result.xlsx neben die jeweilige .xlsx",
)
args = parser.parse_args()
logging.basicConfig(
@@ -165,8 +413,14 @@ def main() -> None:
if args.example_dir is None:
log.info("Beispiel-Verzeichnis (alle Unterordner): %s", examples_root)
data = process_all(examples_root)
data = process_all(
examples_root,
write_json=args.to_json,
write_csv=args.to_csv,
write_xlsx=args.to_xlsx,
)
log.info("Objekt erzeugt mit %d Schluesseln", len(data))
print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
return
# --example-dir: entweder direkter Verzeichnispfad oder Unterordnername in PV_EXAMPLES
@@ -189,8 +443,17 @@ def main() -> None:
log.info("Beispiel-Verzeichnis (einzeln): %s", target_dir)
result: dict[str, list[dict[str, Any]]] = {}
_process_single_folder(target_dir, target_dir.parent, result, log)
_process_single_folder(
target_dir,
target_dir.parent,
result,
log,
write_json=args.to_json,
write_csv=args.to_csv,
write_xlsx=args.to_xlsx,
)
log.info("Objekt erzeugt mit %d Schluesseln", len(result))
#print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
if __name__ == "__main__":