erste Fassung des Extracts von json files aus den Excel Dateien des Kontrolling geschrieben.
This commit is contained in:
@@ -33,7 +33,17 @@ Dieses Repo (`docu_build`) dient als Entwicklungs- und Planungsumgebung. Das Zie
|
||||
|
||||
### Technologie-Stack
|
||||
|
||||
Sphinx + reStructuredText + Jinja2-Templating → LaTeX → PDF. Konfiguration via JSON (ERP-Export) + YAML (Modul-Metadaten). Bildkonvertierung: ezdxf → SVG, Pillow. CI: Gitea Actions oder n8n auf Hetzner-Server.
|
||||
| Komponente | Technologie |
|
||||
|------------------|--------------------------------------------------|
|
||||
| Build-System | Sphinx (Python) |
|
||||
| Textformat | reStructuredText (rST) |
|
||||
| Templating | Jinja2 (Rahmenkapitel, Titelseite) |
|
||||
| PDF-Erzeugung | LaTeX (pdflatex/lualatex) |
|
||||
| Versionierung | Git (Gitea auf Hetzner) |
|
||||
| Konfiguration | JSON (ERP-Export) + YAML (Modul-Metadaten) |
|
||||
| CI/Automatisierung | Gitea Actions oder n8n |
|
||||
| Bildkonvertierung | ezdxf → SVG, Pillow (Optimierung) |
|
||||
|
||||
|
||||
### Build-Pipeline (zu implementieren)
|
||||
|
||||
@@ -49,6 +59,8 @@ Sphinx + reStructuredText + Jinja2-Templating → LaTeX → PDF. Konfiguration v
|
||||
|
||||
**meta.yaml** (pro Modul): Definiert `display_name` (mehrsprachig), `chapters` (Kapitelzuordnung mit Sections), `images` (shared/localized/module_local), `requires` (Abhängigkeiten).
|
||||
|
||||
Details siehe `doc/Kernkonzepte.md`.
|
||||
|
||||
### Kapitelstruktur (`chapters/`)
|
||||
|
||||
```text
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM DOCU_BUILD - Bilder und Text aus Produktprogramm-PDFs extrahieren
|
||||
REM ================================================================
|
||||
|
||||
call "%~dp0setenv.bat"
|
||||
|
||||
if not exist "%PROJECT%\.venv" (
|
||||
echo FEHLER: Virtual environment nicht gefunden.
|
||||
echo Bitte zuerst bin\install_py.bat ausfuehren.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call "%PROJECT%\.venv\Scripts\activate.bat"
|
||||
|
||||
echo.
|
||||
echo Extrahiere Bilder und Text aus Produktprogramm-PDFs ...
|
||||
echo.
|
||||
|
||||
python "%PV_LIB%\create_examples.py" --example-dir fortna %*
|
||||
@@ -0,0 +1,25 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM JSON-Baumstruktur aus Excel-Stueckliste erzeugen
|
||||
REM
|
||||
REM Liest eine Excel-Datei aus examples/ und baut aus der
|
||||
REM Positionsspalte eine hierarchische JSON-Datei.
|
||||
REM
|
||||
REM Verwendung:
|
||||
REM jsonFromXlsx.bat --filename Fortna --tab soll
|
||||
REM jsonFromXlsx.bat --filename Fortna --tab ist
|
||||
REM ================================================================
|
||||
|
||||
call "%~dp0setenv.bat"
|
||||
|
||||
REM Pruefe ob venv existiert
|
||||
if not exist "%PROJECT%\.venv" (
|
||||
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call "%PROJECT%\.venv\Scripts\activate.bat"
|
||||
|
||||
python "%PV_LIB%\jsonFromXlsx.py" %*
|
||||
|
||||
deactivate
|
||||
+106
-8
@@ -386,14 +386,112 @@ Ausgabe:
|
||||
|
||||
## Umsetzungsphasen
|
||||
|
||||
| Phase | Beschreibung | Aufwand |
|
||||
|-------|---------------------------------------------------------------|-----------|
|
||||
| 1 | PoC: Ein Modul, ein Kapitel, zwei Sprachen, PDF | 2–3 Tage |
|
||||
| 2 | Base-Repo aufsetzen: Sphinx, Extensions, LaTeX-CI | 3–5 Tage |
|
||||
| 3 | Erste 5–10 Module migrieren, Glossar aufbauen | 1–2 Wochen|
|
||||
| 4 | JSON-Export aus ERP definieren, build.py fertigstellen | 2–3 Tage |
|
||||
| 5 | Pilotprojekt: Erste vollständige Anlagendokumentation | 1 Woche |
|
||||
| 6 | CI/CD: Automatischer Build via Gitea Actions oder n8n | 1–2 Tage |
|
||||
### Methodik (nach Alistair Cockburn)
|
||||
|
||||
Jeder Use Case durchläuft sieben Schritte bis zur Umsetzungsreife:
|
||||
|
||||
| Schritt | Aktivität | Verantwortung |
|
||||
|---------|--------------------------------------------------------|---------------|
|
||||
| 1 | Titel / Teaser, Prosa-Story | C (Kunde/PO) |
|
||||
| 2 | Stakeholder & Interessen / Anwender-Ziele identifizieren | T (Dev Team) |
|
||||
| 3 | Standard-Ablauf entwerfen (primäres Erfolgsszenario) | M (Member) |
|
||||
| 4 | Ausnahmen und Erweiterungen sammeln | T (Dev Team) |
|
||||
| 5 | Ausnahmen detaillieren, Schneiden in User Stories | M (Member) |
|
||||
| 6 | Business Value schätzen / Aufwand schätzen | T (Dev Team) |
|
||||
| 7 | Review: Prüfe alle Interessen, Definition of Ready | C (Kunde/PO) |
|
||||
|
||||
**Legende Verantwortung:**
|
||||
- **C** = Kunde / Product Owner (N.Sch, M.Ro)
|
||||
- **T** = Dev Team (F.Ve, A.Sch, M.St, D.Fe)
|
||||
- **M** = Einzelnes Dev Team Member
|
||||
|
||||
### Personen
|
||||
|
||||
| Kürzel | Rolle | Schwerpunkt |
|
||||
|--------|------------------------|-----------------------|
|
||||
| N.Sch | Stakeholder / PO | Fachliche Anforderungen |
|
||||
| M.Ro | Stakeholder / PO | Fachliche Anforderungen |
|
||||
| F.Ve | Dev Team | IT und KI |
|
||||
| A.Sch | Dev Team | Redaktion / Inhalte |
|
||||
| M.St | Dev Team | IT und KI |
|
||||
| D.Fe | Dev Team | Redaktion / Inhalte |
|
||||
|
||||
### Produktprogramm-Umfang
|
||||
|
||||
Basis für die Aufwandsschätzung: **1388 eindeutige Produktnummern** in 70 Produktgruppen über 5 Produktfamilien (CPC, GERÜST, ILS 2100, OMNIFLO, Trolleyprogramm). 129 Nummern erscheinen in mehreren Kapiteln.
|
||||
|
||||
### Projektplan
|
||||
|
||||
| Phase | Beschreibung | Aufwand | Beteiligte |
|
||||
|-------|--------------|---------|------------|
|
||||
| 0 | **Use-Case-Definition & Anforderungen** | | |
|
||||
| 0.1 | Story & Teaser: Zieldokumentation beschreiben, Prosa-Story je Produktfamilie | 2 Tage | D.Fe, A.Sch, M.St |
|
||||
| 0.2 | Stakeholder-Analyse: Alle Akteure und deren Interessen/Ziele auflisten | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
|
||||
| 0.3 | Standard-Ablauf entwerfen: Build-Pipeline, Redaktionsworkflow, Freigabeprozess | 2 Tage | M.St |
|
||||
| 0.4 | Ausnahmen sammeln: Sonderfälle (Mehrfachverwendung der 129 Nummern, Varianten, fehlende Übersetzungen) | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
|
||||
| 0.5 | Ausnahmen detaillieren, in User Stories schneiden | 2 Tage | F.Ve, M.St |
|
||||
| 0.6 | Aufwandsschätzung & Business Value für alle Stories | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
|
||||
| 0.7 | Review & DOR: Prüfung aller Interessen, Freigabe für Umsetzung | 1 Tag | N.Sch, M.Ro |
|
||||
| | | | |
|
||||
| 1 | **PoC: Proof of Concept** | | |
|
||||
| 1.1 | Ein Modul (z.B. OMNIFLO Weiche), ein Kapitel, zwei Sprachen (DE/EN), PDF-Ausgabe | 3 Tage | M.St, F.Ve |
|
||||
| 1.2 | LaTeX-Template mit Firmen-CI erstellen und validieren | 2 Tage | M.St, F.Ve, A.Sch |
|
||||
| 1.3 | PoC-Review mit Stakeholdern | 0,5 Tage | N.Sch, M.Ro, A.Sch |
|
||||
| | | | |
|
||||
| 2 | **Base-Repo & Infrastruktur** | | |
|
||||
| 2.1 | Repo-Struktur anlegen (transport-docs-base), Sphinx-Projekt initialisieren | 1 Tag | M.St |
|
||||
| 2.2 | Sphinx-Extensions implementieren (anlagen_builder, localized_figure, glossary_loader) | 4 Tage | M.St |
|
||||
| 2.3 | build.py: JSON-Parsing, Abhängigkeitsauflösung, Override-Mechanismus | 3 Tage | F.Ve, M.St |
|
||||
| 2.4 | CI/CD: Gitea Actions für automatischen Build | 2 Tage | M.St |
|
||||
| | | | |
|
||||
| 3 | **Produktprogramm-Strukturierung** | | |
|
||||
| 3.1 | Modulstruktur & meta.yaml-Schema je Produktfamilie definieren (5 Familien) | 3 Tage | A.Sch, M.St, D.Fe |
|
||||
| 3.2 | Nummernkreis-Mapping: 1388 Produkte auf Module abbilden, Gruppierung festlegen | 3 Tage | A.Sch, F.Ve, M.St |
|
||||
| 3.3 | Vorlage-Templates pro Kapiteltyp erstellen (Beschreibung, Wartung, Ersatzteile) | 2 Tage | A.Sch, M.St |
|
||||
| | | | |
|
||||
| 4 | **Content-Migration (Hauptaufwand)** | | |
|
||||
| 4.1 | CPC-Familie: 126 Produkte in 11 Gruppen – rST-Bausteine erstellen | 2 Wochen | A.Sch, F.Ve |
|
||||
| 4.2 | GERÜST-Familie: 263 Produkte in 14 Gruppen – rST-Bausteine erstellen | 1 Wochen | A.Sch, F.Ve |
|
||||
| 4.3 | ILS 2100-Familie: 396 Produkte in 27 Gruppen – rST-Bausteine erstellen | 3 Wochen | A.Sch, F.Ve |
|
||||
| 4.4 | OMNIFLO-Familie: 453 Produkte in 14 Gruppen – rST-Bausteine erstellen | 4 Wochen | A.Sch, F.Ve |
|
||||
| 4.5 | Trolleyprogramm: 191 Produkte in 4 Gruppen – rST-Bausteine erstellen | 2 Wochen | A.Sch, F.Ve |
|
||||
| 4.6 | KI-gestützte Textgenerierung: Vorlagen automatisiert befüllen (Beschreibungen, techn. Daten, Logische Schalter) | parallel | F.Ve, M.St |
|
||||
| 4.7 | Bildmaterial: Fotos/Zeichnungen zuordnen, optimieren, Pfadstruktur aufbauen | parallel | A.Sch, F.Ve |
|
||||
| | | | |
|
||||
| 5 | **Glossar & Mehrsprachigkeit** | | |
|
||||
| 5.1 | Glossar DE aufbauen (Fachbegriffe aus allen 5 Produktfamilien) | 1 Woche | A.Sch, D.Fe, M.St |
|
||||
| 5.2 | Glossar EN übersetzen | 1 Woche | A.Sch, D.Fe, M.St, V.Fe |
|
||||
| 5.3 | Stichproben-Übersetzung: 10% der Module auf EN migrieren | 2 Wochen | A.Sch, D.Fe, F.Ve |
|
||||
| | | | |
|
||||
| 6 | **ERP-Integration & Automatisierung** | | |
|
||||
| 6.1 | JSON-Export-Format mit ERP abstimmen (anlage.json-Schema) | 2 Tage | M.St, A.Jakob |
|
||||
| 6.2 | Validierungsskripte: meta.yaml-Konsistenz, fehlende Dateien/Übersetzungen | 2 Tage | M.St |
|
||||
| 6.3 | Hilfsskripte: dxf2svg, optimize_images | 2 Tage | F.Ve |
|
||||
| | | | |
|
||||
| 7 | **Pilotprojekt** | | |
|
||||
| 7.1 | Erste vollständige Anlagendokumentation (reales Kundenprojekt) generieren | 1 Woche | A.Sch, M.St, F.Ve |
|
||||
| 7.2 | Review mit Stakeholdern, Korrekturrunde | 3 Tage | N.Sch, M.Ro, A.Sch |
|
||||
| 7.3 | Lessons Learned, Backlog-Pflege | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
|
||||
|
||||
### Aufwandszusammenfassung
|
||||
|
||||
| Phase | Dauer (geschätzt) |
|
||||
|-------|-------------------|
|
||||
| 0 – Use-Case-Definition | ~2 Wochen |
|
||||
| 1 – PoC | ~1 Woche |
|
||||
| 2 – Base-Repo & Infrastruktur | ~2 Wochen |
|
||||
| 3 – Produktprogramm-Strukturierung | ~2 Wochen |
|
||||
| 4 – Content-Migration | ~10-12 Wochen (2 Redakteure parallel + KI-Unterstützung) |
|
||||
| 5 – Glossar & Mehrsprachigkeit | ~4 Wochen |
|
||||
| 6 – ERP-Integration | ~1 Woche |
|
||||
| 7 – Pilotprojekt | ~2 Wochen |
|
||||
| **Gesamt** | **~24 Wochen (6 Monate)** |
|
||||
|
||||
**Anmerkungen:**
|
||||
- Phase 4 ist der Hauptaufwand. Bei 1388 Produkten und 2 Redakteurinnen (A.Sch, F.Ve) parallel ergibt sich ca. 10 Wochen netto für die Content-Erstellung.
|
||||
- KI-gestützte Textgenerierung (F.Ve, M.St) kann den Aufwand in Phase 4 um geschätzt 30–50% reduzieren.
|
||||
- Phasen 4, 5 und 6 können teilweise parallelisiert werden, wodurch die Gesamtlaufzeit auf ca. 5 Monate verkürzt werden kann.
|
||||
- Die Schätzung basiert auf ~20 Produkte/Woche pro Redakteur bei mittlerer Komplexität (Beschreibung, techn. Daten, Wartungshinweise als rST-Bausteine).
|
||||
|
||||
## Coding-Konventionen
|
||||
|
||||
|
||||
@@ -148,3 +148,109 @@ Neues Konzept: Pro Sprache ein kompletter Dokumentenbaum auf Top-Level:
|
||||
- Zukuenftig: `chapters_en/`, `chapters_cz/` etc. als eigenstaendige Kopien
|
||||
|
||||
Fallback-Mechanismus: Fehlt ein Dokument in einer Fremdsprache, wird die deutsche Version verwendet.
|
||||
|
||||
|
||||
## Heutige Checkliste für die nötigen Dokumente aus der Automation
|
||||
|
||||
### 1. Layout Elektrik
|
||||
|
||||
- Elektrolayout
|
||||
- Layout Kabelpritsche und Erdung
|
||||
|
||||
### 2. Schaltplan & Pneumatikplan
|
||||
|
||||
- Schaltplan (WSCAD Elektrisch)
|
||||
- Pneumatikplan (WSCAD Elektrisch)
|
||||
- PDF-Ausgabe
|
||||
- Projektsicherung im WSCAD-Format
|
||||
|
||||
### 3. Layout Pneumatik
|
||||
|
||||
- Verschlauchungsplan
|
||||
|
||||
### 4. Ersatzteilliste Elektrik
|
||||
|
||||
- Ersatzteilliste für elektrische Komponenten als PDF-Dokument
|
||||
|
||||
### 5. Dokumentationen Elektrolieferanten
|
||||
|
||||
- Ablage der Datenblätter
|
||||
- Dokumentationen der Elektrolieferanten
|
||||
|
||||
### 6. Netzwerkstrukturplanung
|
||||
|
||||
- Netzwerkstrukturplanung (ProNetPlan / WSCAD)
|
||||
|
||||
### 7. Betriebsanleitung Steuerung
|
||||
|
||||
- Bedienhandbuch
|
||||
- Steuerungsanleitung
|
||||
|
||||
### 8. Software-Daten SPS
|
||||
|
||||
- SPS-Projektdaten mit Sicherung
|
||||
- PDF-Ausdruck des SPS-Programms
|
||||
|
||||
### 9. Sicherheitsausdruck SPS
|
||||
|
||||
- Sicherheitsausdruck der SPS-Software
|
||||
|
||||
### 10. Prüfprotokoll externe Gewerke
|
||||
|
||||
- Prüfprotokolle für externe Gewerke im eigenen Lieferumfang (z. B. Brandschutztüre)
|
||||
|
||||
### 11. Sistema Sicherheitsbetrachtung
|
||||
|
||||
- Sicherheitsbetrachtung gemäß Sistema als PDF-Dokument
|
||||
|
||||
### 12. Prüfprotokolle Schaltschränke
|
||||
|
||||
- Prüfprotokolle der Schaltschrankprüfung als PDF-Dokument
|
||||
|
||||
### 13. Lizenzen
|
||||
|
||||
- Softwarelizenzen (z. B. Sinamics, TIA Portal, OPC UA)
|
||||
|
||||
### 14. Abschaltmatrix
|
||||
|
||||
- Emergency-Stop-Matrix (Abschaltmatrix) als PDF-Dokument
|
||||
|
||||
### 15. Layout Sicherheitsbereiche
|
||||
|
||||
- Layout der Sicherheitsbereiche zur Abschaltmatrix
|
||||
|
||||
### 16. Validierungsprotokolle
|
||||
|
||||
Unterschriebene Validierungsprotokolle, bestehend aus:
|
||||
|
||||
#### 16.1 Prüfprotokoll ortsfester Arbeitsmittel
|
||||
- Elektrische Prüfung ortsfester Arbeitsmittel
|
||||
|
||||
#### 16.2 Erprobung Wiederanlauf
|
||||
- Schutz gegen automatischen Wiederanlauf
|
||||
|
||||
#### 16.3 Erprobung Drehrichtung Motoren
|
||||
- Test der Drehrichtung der Asynchronmotoren
|
||||
|
||||
#### 16.4 Berechnung Fehlerschleifenimpedanz / Validierung Erdung
|
||||
- Berechnung der Fehlerschleifenimpedanz
|
||||
- Erdungsvalidierung
|
||||
|
||||
#### 16.5 Erprobung Anzeige- und Bediengeräte
|
||||
- Test der Anzeige- und Bediengeräte
|
||||
|
||||
|
||||
# Fragen und TODOs
|
||||
|
||||
- [ ] Wie ist der Standardablauf der Dokumentation bisher?
|
||||
- [ ] Wer ist wann daran bei einem einfach Projekt daran beteiligt?
|
||||
- [ ] Was wäre bei einer Automatisierung der Dokumentations- Erstellung für die wünschenswert?
|
||||
- [ ] Welche Ausnahmen sind üblich, fehlt oft oder ist nicht immer dabei?
|
||||
- [ ] für wen ist die Dokumentation gedacht, bzw. was erwarten diese Leute zu finden?
|
||||
- [ ] Welche Detailierungstiefe bei den Produkten ist nötig oder gewollt?
|
||||
- [ ] soll die aktuelle Kapitelstruktur mit nur 4 Kapiteln beibehalten werden oder könnten wir dies auf ca. 7 - 10 Kapitel erweitern (siehe Vorschlag)?
|
||||
- [ ] in welchem Kapitel sollen die jeweiligen Dokumente aus der Automation landen?
|
||||
- [ ] Wie läuft die Ersatzteilfindung nach der Dokumentation statt?
|
||||
- [ ] Wie die Behandlung einer Anlage bei Fehler oder Störungen?
|
||||
- [ ] Welche Daten des Produktprogrammes sollen in die Dokumentation eingang finden?
|
||||
- [ ] Welche Abteilung liefert normalerweise was in der Dokumentation (Liste Automation ist vorhanden, Was passiert bei Sonderkonstruktionen)?
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Builds a JSON hierarchy tree from the Position column of an Excel Stk.-Liste.
|
||||
|
||||
Usage:
|
||||
python jsonFromXlsx.py --filename Fortna --tab soll
|
||||
python jsonFromXlsx.py --filename Fortna --tab ist --output result.json
|
||||
python jsonFromXlsx.py --filename KA135776 --extract-ids
|
||||
python jsonFromXlsx.py --from-results --extract-ids
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
|
||||
|
||||
SHEET_NAMES = {
|
||||
"soll": "Stk.-Liste SOLL",
|
||||
"ist": "Stk.-Liste IST",
|
||||
}
|
||||
|
||||
# Column indices (0-based) in the header row
|
||||
COL_POSITION = 0
|
||||
COL_TEILENUMMER = 1
|
||||
COL_BEZEICHNUNG = 2
|
||||
HEADER_ROW = 3 # 1-based row number of the header
|
||||
|
||||
|
||||
def find_xlsx(examples_dir: str, pattern: str) -> Path:
|
||||
"""Find an .xlsx file in examples/ whose name contains the given pattern."""
|
||||
matches = []
|
||||
for f in Path(examples_dir).glob("*.xlsx"):
|
||||
if pattern.lower() in f.name.lower():
|
||||
matches.append(f)
|
||||
if not matches:
|
||||
print(f"FEHLER: Keine .xlsx-Datei mit Muster '{pattern}' in {examples_dir} gefunden.")
|
||||
sys.exit(1)
|
||||
if len(matches) > 1:
|
||||
print(f"Mehrere Dateien gefunden fuer Muster '{pattern}':")
|
||||
for m in sorted(matches):
|
||||
print(f" {m.name}")
|
||||
print(f"Verwende: {matches[0].name}")
|
||||
return sorted(matches)[0]
|
||||
|
||||
|
||||
def make_key(teilenummer: str, bezeichnung: str, is_unique: bool) -> str:
|
||||
"""Build a node key from Teilenummer, adding Bezeichnung if not unique."""
|
||||
if is_unique:
|
||||
return teilenummer
|
||||
combined = f"{teilenummer}-{bezeichnung}"
|
||||
return combined.replace(" ", "_")
|
||||
|
||||
|
||||
def unique_key(parent: dict, key: str) -> str:
|
||||
"""Return key with _1, _2, ... suffix if it already exists in parent."""
|
||||
if key not in parent:
|
||||
return key
|
||||
suffix = 1
|
||||
while f"{key}_{suffix}" in parent:
|
||||
suffix += 1
|
||||
return f"{key}_{suffix}"
|
||||
|
||||
|
||||
def build_tree(ws, sheet_type: str) -> dict:
|
||||
"""Read the worksheet and build a nested dict from Position paths.
|
||||
|
||||
Keys are Teilenummer-based (with Bezeichnung if non-unique).
|
||||
Position paths are used only for hierarchy, not as keys.
|
||||
"""
|
||||
# First pass: collect all Teilenummern to determine uniqueness
|
||||
teil_counter: Counter = Counter()
|
||||
rows_data = []
|
||||
for row in ws.iter_rows(min_row=HEADER_ROW + 1, max_row=ws.max_row, values_only=True):
|
||||
pos = row[COL_POSITION]
|
||||
teil = row[COL_TEILENUMMER]
|
||||
bez = row[COL_BEZEICHNUNG]
|
||||
if pos is None or teil is None:
|
||||
continue
|
||||
pos_str = str(pos).strip()
|
||||
if pos_str.lower() == "position":
|
||||
continue # skip header row
|
||||
teil_str = str(teil).strip()
|
||||
bez_str = str(bez).strip() if bez else ""
|
||||
teil_counter[teil_str] += 1
|
||||
rows_data.append((pos_str, teil_str, bez_str))
|
||||
|
||||
unique_teile = {t for t, count in teil_counter.items() if count == 1}
|
||||
|
||||
# Second pass: build tree with Teilenummer-based keys
|
||||
tree: dict = {}
|
||||
pos_to_node: dict[str, dict] = {} # maps position path -> dict node
|
||||
# Track metadata per node for leaf post-processing
|
||||
node_meta: dict[int, tuple[str, str]] = {} # id(child) -> (teil_str, bez_str)
|
||||
|
||||
for pos_str, teil_str, bez_str in rows_data:
|
||||
key = make_key(teil_str, bez_str, teil_str in unique_teile)
|
||||
segments = pos_str.split("/")
|
||||
|
||||
# Find parent dict
|
||||
if len(segments) == 1:
|
||||
parent = tree
|
||||
else:
|
||||
parent_pos = "/".join(segments[:-1])
|
||||
if parent_pos not in pos_to_node:
|
||||
# Parent row missing in data, create placeholder
|
||||
pos_to_node[parent_pos] = {}
|
||||
parent = pos_to_node[parent_pos]
|
||||
|
||||
# Insert with unique key
|
||||
final_key = unique_key(parent, key)
|
||||
child: dict = {}
|
||||
parent[final_key] = child
|
||||
pos_to_node[pos_str] = child
|
||||
node_meta[id(child)] = (teil_str, bez_str)
|
||||
|
||||
is_sivas = re.compile(r"^\d{9}$").match
|
||||
|
||||
# Post-process: leaves (empty dicts) -> key=Teilenummer, value=Bezeichnung
|
||||
def simplify_leaves(node: dict) -> dict:
|
||||
result: dict = {}
|
||||
for key, value in node.items():
|
||||
if isinstance(value, dict) and len(value) == 0:
|
||||
# Leaf node: use only Teilenummer as key, Bezeichnung as value
|
||||
teil, bez = node_meta.get(id(value), ("", ""))
|
||||
if teil in result and is_sivas(teil):
|
||||
# Aggregate duplicate 9-digit Sivas numbers
|
||||
existing = result[teil]
|
||||
if isinstance(existing, str):
|
||||
result[teil] = {"anzahl": 2, "beschreibung": existing}
|
||||
else:
|
||||
existing["anzahl"] += 1
|
||||
else:
|
||||
leaf_key = unique_key(result, teil)
|
||||
result[leaf_key] = bez
|
||||
elif isinstance(value, dict):
|
||||
result[key] = simplify_leaves(value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
return simplify_leaves(tree)
|
||||
|
||||
|
||||
RE_SIVAS = re.compile(r"^(\d{9})")
|
||||
|
||||
|
||||
def is_leaf_value(v) -> bool:
|
||||
"""True if v is a leaf entry (string or anzahl-dict)."""
|
||||
if isinstance(v, str):
|
||||
return True
|
||||
if isinstance(v, dict) and "anzahl" in v:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_ids_from_tree(tree: dict) -> dict[str, str]:
|
||||
"""Collect 9-digit Sivas numbers one level above leaves.
|
||||
|
||||
Returns dict of nummer -> bezeichnung.
|
||||
"""
|
||||
ids: dict[str, str] = {}
|
||||
|
||||
def walk(node: dict):
|
||||
for key, value in node.items():
|
||||
if not isinstance(value, dict) or "anzahl" in value:
|
||||
continue # leaf, skip
|
||||
# Check if all children are leaves
|
||||
all_leaves = all(is_leaf_value(v) for v in value.values())
|
||||
m = RE_SIVAS.match(key)
|
||||
if all_leaves and value and m:
|
||||
nummer = m.group(1)
|
||||
# Bezeichnung from key: part after "nummer-", underscores back to spaces
|
||||
if "-" in key[9:]:
|
||||
bez = key[10:].replace("_", " ")
|
||||
elif len(key) > 9:
|
||||
bez = key[10:].replace("_", " ")
|
||||
else:
|
||||
bez = nummer
|
||||
ids[nummer] = bez
|
||||
else:
|
||||
walk(value)
|
||||
|
||||
walk(tree)
|
||||
return ids
|
||||
|
||||
|
||||
def process_file(xlsx_path: Path, sheet_name: str, tab: str, results_dir: Path,
|
||||
out_path: Path | None = None) -> bool:
|
||||
"""Process a single xlsx file. Returns True on success."""
|
||||
print(f"Lade: {xlsx_path.name}")
|
||||
print(f"Sheet: {sheet_name}")
|
||||
|
||||
wb = openpyxl.load_workbook(str(xlsx_path), data_only=True)
|
||||
if sheet_name not in wb.sheetnames:
|
||||
print(f" SKIP: Sheet '{sheet_name}' nicht vorhanden.")
|
||||
return False
|
||||
|
||||
ws = wb[sheet_name]
|
||||
tree = build_tree(ws, tab)
|
||||
|
||||
if out_path is None:
|
||||
stem = xlsx_path.stem.replace(" ", "_")
|
||||
out_path = results_dir / f"{stem}_{tab}.json"
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(tree, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f" Ausgabe: {out_path}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Erzeugt eine JSON-Baumstruktur aus der Positionsspalte einer Excel-Stueckliste."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filename",
|
||||
default=None,
|
||||
help="Suchmuster fuer die .xlsx-Datei im examples/-Ordner (z.B. 'Fortna')",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tab",
|
||||
choices=["soll", "ist"],
|
||||
default="soll",
|
||||
help="Welches Tabellenblatt gelesen wird: 'soll' oder 'ist' (default: soll)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default=None,
|
||||
help="Ausgabedatei (default: <filename>_<tab>.json im results/-Ordner)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
dest="process_all",
|
||||
help="Alle .xlsx-Dateien im examples/-Ordner in _ist und _soll JSON konvertieren",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extract-ids",
|
||||
action="store_true",
|
||||
dest="extract_ids",
|
||||
help="9-stellige Sivas-Nummern eine Ebene ueber den Blaettern extrahieren",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--from-results",
|
||||
action="store_true",
|
||||
dest="from_results",
|
||||
help="JSON-Dateien aus results/ lesen statt aus .xlsx zu erzeugen",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.process_all and not args.filename and not args.from_results:
|
||||
parser.error("Entweder --filename, --all oder --from-results muss angegeben werden.")
|
||||
|
||||
# Resolve paths relative to project root
|
||||
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
|
||||
examples_dir = project_root / "examples"
|
||||
results_dir = project_root / "results"
|
||||
results_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Default tab to "ist" when --extract-ids without explicit --tab
|
||||
tab = args.tab
|
||||
if args.extract_ids and args.tab == "soll" and "--tab" not in sys.argv:
|
||||
tab = "ist"
|
||||
|
||||
if args.from_results:
|
||||
# Read existing JSON files from results/
|
||||
pattern = f"*_{tab}.json"
|
||||
json_files = sorted(results_dir.glob(pattern))
|
||||
if args.filename:
|
||||
json_files = [f for f in json_files if args.filename.lower() in f.name.lower()]
|
||||
if not json_files:
|
||||
print(f"FEHLER: Keine JSON-Dateien mit Muster '{pattern}' in {results_dir} gefunden.")
|
||||
sys.exit(1)
|
||||
print(f"Lese {len(json_files)} JSON-Dateien aus {results_dir}\n")
|
||||
for json_path in json_files:
|
||||
with open(json_path, encoding="utf-8") as f:
|
||||
tree = json.load(f)
|
||||
# Derive project name from filename: strip _ist/_soll.json suffix
|
||||
project_name = json_path.stem
|
||||
for suffix in ("_ist", "_soll"):
|
||||
if project_name.endswith(suffix):
|
||||
project_name = project_name[: -len(suffix)]
|
||||
break
|
||||
if args.extract_ids:
|
||||
ids = extract_ids_from_tree(tree)
|
||||
ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
|
||||
with open(ids_path, "w", encoding="utf-8") as f:
|
||||
for nummer, bez in sorted(ids.items()):
|
||||
f.write(f"{nummer}: {bez}\n")
|
||||
print(f" {json_path.name} -> {ids_path.name} ({len(ids)} IDs)")
|
||||
elif args.process_all:
|
||||
xlsx_files = sorted(f for f in Path(examples_dir).glob("*.xlsx")
|
||||
if not f.name.startswith("~$"))
|
||||
print(f"Verarbeite {len(xlsx_files)} Dateien aus {examples_dir}\n")
|
||||
ok, skip = 0, 0
|
||||
for xlsx_path in xlsx_files:
|
||||
for t in ("soll", "ist"):
|
||||
sheet_name = SHEET_NAMES[t]
|
||||
if process_file(xlsx_path, sheet_name, t, results_dir):
|
||||
ok += 1
|
||||
else:
|
||||
skip += 1
|
||||
print()
|
||||
print(f"Fertig: {ok} JSON erzeugt, {skip} uebersprungen.")
|
||||
else:
|
||||
xlsx_path = find_xlsx(str(examples_dir), args.filename)
|
||||
sheet_name = SHEET_NAMES[tab]
|
||||
|
||||
out_path = None
|
||||
if args.output:
|
||||
out_path = Path(args.output)
|
||||
if not out_path.is_absolute():
|
||||
out_path = results_dir / out_path
|
||||
|
||||
json_out = None
|
||||
if args.extract_ids:
|
||||
stem = xlsx_path.stem.replace(" ", "_")
|
||||
json_out = results_dir / f"{stem}_{tab}.json"
|
||||
|
||||
if not process_file(xlsx_path, sheet_name, tab, results_dir, out_path):
|
||||
sys.exit(1)
|
||||
|
||||
if args.extract_ids:
|
||||
# Read back the generated JSON
|
||||
read_path = out_path or json_out or results_dir / f"{xlsx_path.stem.replace(' ', '_')}_{tab}.json"
|
||||
with open(read_path, encoding="utf-8") as f:
|
||||
tree = json.load(f)
|
||||
ids = extract_ids_from_tree(tree)
|
||||
project_name = xlsx_path.stem.replace(" ", "_")
|
||||
ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
|
||||
with open(ids_path, "w", encoding="utf-8") as f:
|
||||
for nummer, bez in sorted(ids.items()):
|
||||
f.write(f"{nummer}: {bez}\n")
|
||||
print(f" IDs: {ids_path} ({len(ids)} IDs)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user