Hilfskripte zum Extrakt aus dem Produktprogramm erstellt. Kernkonzepte in README.md und doc
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
create_examples.py - Liest Stuecklisten-Daten aus Excel-Dateien im Examples-Ordner.
|
||||
|
||||
Verwendung:
|
||||
python create_examples.py [--example-dir <pfad>]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StkItem:
|
||||
position: str
|
||||
teilenummer: str
|
||||
menge: Any
|
||||
|
||||
|
||||
def _cell_str(value: Any) -> str:
|
||||
return str(value).strip() if value is not None else ""
|
||||
|
||||
|
||||
def _find_col_index(header_row: tuple[Any, ...], candidates: list[str]) -> int | None:
|
||||
normalized = [_cell_str(v).lower() for v in header_row]
|
||||
for candidate in candidates:
|
||||
candidate_norm = candidate.lower()
|
||||
if candidate_norm in normalized:
|
||||
return normalized.index(candidate_norm)
|
||||
return None
|
||||
|
||||
|
||||
def _extract_stk_liste_ist(xlsx_path: Path) -> list[StkItem]:
|
||||
import openpyxl
|
||||
|
||||
wb = openpyxl.load_workbook(str(xlsx_path), read_only=True, data_only=True)
|
||||
if "Stk.-Liste IST" not in wb.sheetnames:
|
||||
wb.close()
|
||||
raise ValueError("Reiter 'Stk.-Liste IST' nicht gefunden")
|
||||
|
||||
ws = wb["Stk.-Liste IST"]
|
||||
|
||||
header_idx: tuple[int | None, int | None, int | None] | None = None
|
||||
items: list[StkItem] = []
|
||||
|
||||
for row in ws.iter_rows(values_only=True):
|
||||
if header_idx is None:
|
||||
pos_idx = _find_col_index(list(row), ["position", "pos", "pos."])
|
||||
part_idx = _find_col_index(
|
||||
list(row),
|
||||
["teilenummer", "teilnummer", "article", "artikelnummer"],
|
||||
)
|
||||
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)
|
||||
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
|
||||
|
||||
position = _cell_str(row[pos_idx]) if pos_idx < len(row) else ""
|
||||
teilenummer = _cell_str(row[part_idx]) if part_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):
|
||||
continue
|
||||
if not position or not teilenummer:
|
||||
continue
|
||||
|
||||
items.append(StkItem(position=position, teilenummer=teilenummer, menge=menge))
|
||||
|
||||
wb.close()
|
||||
return items
|
||||
|
||||
|
||||
def process_all(example_dir: Path) -> dict[str, list[dict[str, Any]]]:
|
||||
"""
|
||||
Durchsucht rekursiv alle Unterverzeichnisse in example_dir.
|
||||
|
||||
Pro Unterverzeichnis wird die erste gefundene .xlsx-Datei gelesen und aus
|
||||
dem Reiter "Stk.-Liste IST" werden Position, Teilenummer, Menge extrahiert.
|
||||
"""
|
||||
if not example_dir.exists():
|
||||
log.error("Beispiel-Verzeichnis existiert nicht: %s", example_dir)
|
||||
sys.exit(1)
|
||||
|
||||
subdirs = sorted([p for p in example_dir.rglob("*") if p.is_dir()])
|
||||
if not subdirs:
|
||||
log.error("Keine Unterverzeichnisse in %s gefunden", example_dir)
|
||||
sys.exit(1)
|
||||
|
||||
log.info("Gefunden: %d Unterverzeichnisse in %s", len(subdirs), example_dir)
|
||||
|
||||
result: dict[str, list[dict[str, Any]]] = {}
|
||||
for folder in subdirs:
|
||||
xlsx_files = sorted(folder.glob("*.xlsx"))
|
||||
if not xlsx_files:
|
||||
continue
|
||||
|
||||
xlsx_path = xlsx_files[0]
|
||||
rel_folder = str(folder.relative_to(example_dir))
|
||||
log.info("Verarbeite %s: %s", rel_folder, xlsx_path.name)
|
||||
try:
|
||||
items = _extract_stk_liste_ist(xlsx_path)
|
||||
except ValueError as exc:
|
||||
log.warning("%s/%s uebersprungen: %s", rel_folder, xlsx_path.name, exc)
|
||||
continue
|
||||
|
||||
result[rel_folder] = [
|
||||
{
|
||||
"position": item.position,
|
||||
"teilenummer": item.teilenummer,
|
||||
"menge": item.menge,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
log.info("Fertig. Gelesene Verzeichnisse mit Daten: %d", len(result))
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Liest Position/Teilenummer/Menge aus Example-Exceldateien"
|
||||
)
|
||||
parser.add_argument("--example-dir", type=Path, default=None,
|
||||
help="example/ Verzeichnis (default: aus PV_EXAMPLES oder ./examples)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true",
|
||||
help="Ausfuehrliche Ausgabe")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format="%(levelname)-7s %(message)s",
|
||||
)
|
||||
|
||||
import os
|
||||
if args.example_dir:
|
||||
example_dir = args.example_dir.resolve()
|
||||
elif os.environ.get("PV_EXAMPLES"):
|
||||
example_dir = Path(os.environ["PV_EXAMPLES"]).resolve()
|
||||
else:
|
||||
example_dir = Path("example").resolve()
|
||||
|
||||
log.info("Beispiel-Verzeichnis: %s", example_dir)
|
||||
data = process_all(example_dir)
|
||||
log.info("Objekt erzeugt mit %d Schluesseln", len(data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user