gaeb-cli 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gaeb_cli/__init__.py +3 -0
- gaeb_cli/main.py +430 -0
- gaeb_cli-0.5.2.dist-info/METADATA +141 -0
- gaeb_cli-0.5.2.dist-info/RECORD +6 -0
- gaeb_cli-0.5.2.dist-info/WHEEL +4 -0
- gaeb_cli-0.5.2.dist-info/entry_points.txt +3 -0
gaeb_cli/__init__.py
ADDED
gaeb_cli/main.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""gaeb-cli – Kommandozeilen-Tool für GAEB-Dateien."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from gaeb.io.excel.reader import read as read_excel
|
|
10
|
+
from gaeb.io.excel.writer import write as write_excel
|
|
11
|
+
from gaeb.io.gaeb90.reader import read as read_gaeb90
|
|
12
|
+
from gaeb.io.gaeb90.writer import write as write_gaeb90
|
|
13
|
+
from gaeb.io.gaeb2000.reader import read as read_gaeb2000
|
|
14
|
+
from gaeb.io.gaeb2000.writer import write as write_gaeb2000
|
|
15
|
+
from gaeb.io.xml.reader import read as read_xml
|
|
16
|
+
from gaeb.io.xml.writer import write as write_xml
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
import gaeb
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fmt_feldwert(wert: object) -> str:
|
|
25
|
+
"""Formatiert einen Diff-Feldwert für die Tabellenausgabe."""
|
|
26
|
+
if isinstance(wert, list):
|
|
27
|
+
zeilen = [str(z) for z in wert]
|
|
28
|
+
if len(zeilen) <= 2:
|
|
29
|
+
return " / ".join(zeilen)
|
|
30
|
+
return " / ".join(zeilen[:2]) + f" … (+{len(zeilen) - 2})"
|
|
31
|
+
return str(wert)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(
|
|
35
|
+
name="gaeb",
|
|
36
|
+
help="Lesen, Schreiben und Konvertieren von GAEB-Dateien (AVA).",
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
console = Console()
|
|
40
|
+
err = Console(stderr=True, style="bold red")
|
|
41
|
+
|
|
42
|
+
# Bekannte Dateiendungen
|
|
43
|
+
_GAEB90_EXT = {
|
|
44
|
+
".d80", ".d81", ".d82", ".d83", ".d84", ".d85", ".d86",
|
|
45
|
+
".d87", ".d88", ".d89",
|
|
46
|
+
}
|
|
47
|
+
_GAEB2000_EXT = {
|
|
48
|
+
".p80", ".p81", ".p82", ".p83", ".p84", ".p85", ".p86",
|
|
49
|
+
".p87", ".p88", ".p89",
|
|
50
|
+
}
|
|
51
|
+
_XML_EXT = {
|
|
52
|
+
# Mengenermittlung / Kosten
|
|
53
|
+
".x31", ".x50", ".x51", ".x52",
|
|
54
|
+
# Procurement
|
|
55
|
+
".x80", ".x81", ".x82", ".x83", ".x84", ".x85", ".x86",
|
|
56
|
+
".x87", ".x88", ".x89",
|
|
57
|
+
# Trade
|
|
58
|
+
".x93", ".x94", ".x95", ".x96", ".x97",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _detect_format(path: Path) -> str:
|
|
63
|
+
ext = path.suffix.lower()
|
|
64
|
+
if ext in _GAEB90_EXT:
|
|
65
|
+
return "gaeb90"
|
|
66
|
+
if ext in _GAEB2000_EXT:
|
|
67
|
+
return "gaeb2000"
|
|
68
|
+
if ext in _XML_EXT:
|
|
69
|
+
return "xml"
|
|
70
|
+
if ext == ".xlsx":
|
|
71
|
+
return "excel"
|
|
72
|
+
err.print(f"[red]Unbekannte Dateiendung: {ext}[/red]")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read(path: Path, encoding: str | None = None) -> gaeb.Leistungsverzeichnis:
|
|
77
|
+
if not path.exists():
|
|
78
|
+
err.print(f"Datei nicht gefunden: {path}")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
fmt = _detect_format(path)
|
|
81
|
+
try:
|
|
82
|
+
if fmt == "excel":
|
|
83
|
+
return read_excel(path)
|
|
84
|
+
if fmt == "gaeb90":
|
|
85
|
+
return read_gaeb90(path, encoding=encoding)
|
|
86
|
+
if fmt == "gaeb2000":
|
|
87
|
+
return read_gaeb2000(path)
|
|
88
|
+
return read_xml(path)
|
|
89
|
+
except PermissionError:
|
|
90
|
+
err.print(f"Keine Leseberechtigung: {path}")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
err.print(f"Fehler beim Lesen von {path.name}: {type(exc).__name__}: {exc}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------ #
|
|
98
|
+
# gaeb info <datei>
|
|
99
|
+
# ------------------------------------------------------------------ #
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def info(
|
|
103
|
+
datei: Path = typer.Argument(..., help="GAEB-Datei (.d83, .x83, ...)"),
|
|
104
|
+
encoding: str | None = typer.Option(None, "--encoding", "-e", help="Zeichenkodierung (z.B. cp850, cp1252). Nur für GAEB 90."),
|
|
105
|
+
):
|
|
106
|
+
"""Zeigt Metadaten einer GAEB-Datei."""
|
|
107
|
+
lv = _read(datei, encoding=encoding)
|
|
108
|
+
|
|
109
|
+
ext = datei.suffix.lower()
|
|
110
|
+
if ext in _GAEB90_EXT:
|
|
111
|
+
fmt = "GAEB 90"
|
|
112
|
+
elif ext in _GAEB2000_EXT:
|
|
113
|
+
fmt = "GAEB 2000"
|
|
114
|
+
else:
|
|
115
|
+
fmt = f"GAEB DA XML{f' {lv.xml_version}' if lv.xml_version else ''}"
|
|
116
|
+
|
|
117
|
+
console.print()
|
|
118
|
+
console.print(f"[bold]Datei:[/bold] {datei.name}")
|
|
119
|
+
fmt_enc = f"{fmt} [dim](Encoding: {lv.encoding})[/dim]" if lv.encoding else fmt
|
|
120
|
+
console.print(f"[bold]Format:[/bold] {fmt_enc}")
|
|
121
|
+
console.print(f"[bold]Phase:[/bold] {lv.phase.value if lv.phase else '—'} ({lv.phase.name if lv.phase else '—'})")
|
|
122
|
+
console.print(f"[bold]LV-Titel:[/bold] {lv.titel_lv or '—'}")
|
|
123
|
+
console.print(f"[bold]Projekt:[/bold] {lv.projekt or '—'}")
|
|
124
|
+
console.print(f"[bold]Auftraggeber:[/bold] {lv.auftraggeber or '—'}")
|
|
125
|
+
if lv.auftragnehmer:
|
|
126
|
+
console.print(f"[bold]Auftragnehmer:[/bold] {lv.auftragnehmer}")
|
|
127
|
+
console.print(f"[bold]Währung:[/bold] {lv.waehrung}")
|
|
128
|
+
console.print(f"[bold]Titel:[/bold] {len(lv.titel)} (top-level)")
|
|
129
|
+
console.print(f"[bold]Positionen:[/bold] {len(lv.alle_positionen)} gesamt")
|
|
130
|
+
if lv.vorbemerkungen:
|
|
131
|
+
nicht_leer = [z for z in lv.vorbemerkungen if z.strip()]
|
|
132
|
+
console.print(f"[bold]Vorbemerkungen:[/bold] {len(nicht_leer)} Zeilen")
|
|
133
|
+
gs = lv.gesamtsumme
|
|
134
|
+
if gs is not None:
|
|
135
|
+
console.print(f"[bold]Gesamtsumme:[/bold] {gs:,.2f} {lv.waehrung}")
|
|
136
|
+
console.print()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------ #
|
|
140
|
+
# gaeb show <datei>
|
|
141
|
+
# ------------------------------------------------------------------ #
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def show(
|
|
145
|
+
datei: Path = typer.Argument(..., help="GAEB-Datei (.d83, .x83, ...)"),
|
|
146
|
+
langtext: bool = typer.Option(False, "--langtext", "-l", help="Langtext anzeigen"),
|
|
147
|
+
encoding: str | None = typer.Option(None, "--encoding", "-e", help="Zeichenkodierung (z.B. cp850, cp1252). Nur für GAEB 90."),
|
|
148
|
+
):
|
|
149
|
+
"""Zeigt alle Positionen als Tabelle."""
|
|
150
|
+
lv = _read(datei, encoding=encoding)
|
|
151
|
+
|
|
152
|
+
# Zeitvertrag-Felder nur anzeigen wenn mindestens eine Position sie hat
|
|
153
|
+
hat_z_felder = any(
|
|
154
|
+
p.periodqty is not None or p.min_menge is not None or p.max_menge is not None
|
|
155
|
+
for p in lv.alle_positionen
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
table = Table(
|
|
159
|
+
title=f"{lv.titel_lv or datei.name} [dim](Phase {lv.phase.value if lv.phase else '?'})[/dim]",
|
|
160
|
+
box=box.SIMPLE_HEAD,
|
|
161
|
+
show_lines=langtext,
|
|
162
|
+
)
|
|
163
|
+
table.add_column("OZ", style="cyan", no_wrap=True)
|
|
164
|
+
table.add_column("Kurztext")
|
|
165
|
+
table.add_column("Menge", justify="right", style="yellow")
|
|
166
|
+
table.add_column("Einheit", justify="center")
|
|
167
|
+
if hat_z_felder:
|
|
168
|
+
table.add_column("PeriodQty", justify="right", style="dim yellow")
|
|
169
|
+
table.add_column("MinQty", justify="right", style="dim yellow")
|
|
170
|
+
table.add_column("MaxQty", justify="right", style="dim yellow")
|
|
171
|
+
table.add_column("EP", justify="right", style="green")
|
|
172
|
+
table.add_column("GP", justify="right", style="bold green")
|
|
173
|
+
|
|
174
|
+
for pos in lv.alle_positionen:
|
|
175
|
+
ep = f"{pos.einheitspreis:,.3f}" if pos.einheitspreis is not None else "—"
|
|
176
|
+
gp = f"{pos.gesamtpreis:,.2f}" if pos.gesamtpreis is not None else "—"
|
|
177
|
+
menge = f"{pos.menge:,}" if pos.menge is not None else "—"
|
|
178
|
+
kurztext = pos.kurztext
|
|
179
|
+
if langtext and pos.langtext:
|
|
180
|
+
kurztext += "\n[dim]" + "\n".join(pos.langtext[:3]) + "[/dim]"
|
|
181
|
+
zeile = [pos.oz, kurztext, menge, pos.einheit]
|
|
182
|
+
if hat_z_felder:
|
|
183
|
+
zeile.append(f"{pos.periodqty:,}" if pos.periodqty is not None else "—")
|
|
184
|
+
zeile.append(f"{pos.min_menge:,}" if pos.min_menge is not None else "—")
|
|
185
|
+
zeile.append(f"{pos.max_menge:,}" if pos.max_menge is not None else "—")
|
|
186
|
+
zeile.extend([ep, gp])
|
|
187
|
+
table.add_row(*zeile)
|
|
188
|
+
|
|
189
|
+
console.print()
|
|
190
|
+
console.print(table)
|
|
191
|
+
gs = lv.gesamtsumme
|
|
192
|
+
if gs is not None:
|
|
193
|
+
console.print(f" [bold]Gesamtsumme: {gs:,.2f} {lv.waehrung}[/bold]")
|
|
194
|
+
console.print()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------ #
|
|
198
|
+
# gaeb convert <quelle> <ziel>
|
|
199
|
+
# ------------------------------------------------------------------ #
|
|
200
|
+
|
|
201
|
+
@app.command()
|
|
202
|
+
def convert(
|
|
203
|
+
quelle: Path = typer.Argument(..., help="Quelldatei"),
|
|
204
|
+
ziel: Path = typer.Argument(..., help="Zieldatei (Format aus Endung abgeleitet)"),
|
|
205
|
+
phase: str | None = typer.Option(
|
|
206
|
+
None, "--phase", "-p",
|
|
207
|
+
help="Zielphase überschreiben (z.B. 84 für Angebotsabgabe)"
|
|
208
|
+
),
|
|
209
|
+
lese_encoding: str | None = typer.Option(None, "--lese-encoding", help="Lesekodierung für GAEB-90-Quelldatei (z.B. cp850, cp1252)."),
|
|
210
|
+
schreib_encoding: str | None = typer.Option(None, "--schreib-encoding", help="Schreibkodierung für GAEB-90-Zieldatei (Standard: cp850)."),
|
|
211
|
+
):
|
|
212
|
+
"""Konvertiert eine GAEB-Datei in ein anderes Format oder eine andere Phase."""
|
|
213
|
+
lv = _read(quelle, encoding=lese_encoding)
|
|
214
|
+
|
|
215
|
+
# Phase überschreiben (z.B. D83 → D84)
|
|
216
|
+
if phase:
|
|
217
|
+
try:
|
|
218
|
+
lv.phase = gaeb.Phase(phase)
|
|
219
|
+
except ValueError:
|
|
220
|
+
gueltig = ", ".join(p.value for p in gaeb.Phase)
|
|
221
|
+
err.print(f"Unbekannte Phase: {phase}. Gültig: {gueltig}")
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
else:
|
|
224
|
+
# Phase aus Zieldatei-Endung ableiten
|
|
225
|
+
ziel_ext = ziel.suffix.lower()
|
|
226
|
+
# Extrahiere 2 Ziffern aus der Endung (z.B. "83" aus ".d83", ".x83", ".p83")
|
|
227
|
+
match = re.search(r"(\d{2})", ziel_ext)
|
|
228
|
+
if match:
|
|
229
|
+
ziel_phase = match.group(1)
|
|
230
|
+
try:
|
|
231
|
+
lv.phase = gaeb.Phase(ziel_phase)
|
|
232
|
+
except ValueError:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
ziel_fmt = _detect_format(ziel)
|
|
236
|
+
try:
|
|
237
|
+
if ziel_fmt == "gaeb90":
|
|
238
|
+
write_gaeb90(lv, ziel, encoding=schreib_encoding or "cp850")
|
|
239
|
+
elif ziel_fmt == "gaeb2000":
|
|
240
|
+
write_gaeb2000(lv, ziel, encoding=schreib_encoding or "cp1252")
|
|
241
|
+
elif ziel_fmt == "excel":
|
|
242
|
+
write_excel(lv, ziel)
|
|
243
|
+
else:
|
|
244
|
+
write_xml(lv, ziel)
|
|
245
|
+
except ImportError as exc:
|
|
246
|
+
err.print(f"{exc}")
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
except PermissionError:
|
|
249
|
+
err.print(f"Keine Schreibberechtigung: {ziel}")
|
|
250
|
+
raise typer.Exit(1)
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
err.print(f"Fehler beim Schreiben von {ziel.name}: {type(exc).__name__}: {exc}")
|
|
253
|
+
raise typer.Exit(1)
|
|
254
|
+
|
|
255
|
+
console.print(f"[green]✓[/green] Konvertiert: {quelle.name} → {ziel.name} (Phase {lv.phase.value if lv.phase else '?'})")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------ #
|
|
259
|
+
# gaeb struktur <datei>
|
|
260
|
+
# ------------------------------------------------------------------ #
|
|
261
|
+
|
|
262
|
+
@app.command()
|
|
263
|
+
def struktur(
|
|
264
|
+
datei: Path = typer.Argument(..., help="GAEB-Datei"),
|
|
265
|
+
encoding: str | None = typer.Option(None, "--encoding", "-e", help="Zeichenkodierung (z.B. cp850, cp1252). Nur für GAEB 90."),
|
|
266
|
+
):
|
|
267
|
+
"""Zeigt die Titelstruktur (Hierarchie) eines LV."""
|
|
268
|
+
lv = _read(datei, encoding=encoding)
|
|
269
|
+
|
|
270
|
+
def _drucke(titel, tiefe: int = 0) -> None:
|
|
271
|
+
prefix = " " * tiefe
|
|
272
|
+
marker = "├─" if tiefe > 0 else "●"
|
|
273
|
+
pos_info = f" [dim]({len(titel.positionen)} Pos.)[/dim]" if titel.positionen else ""
|
|
274
|
+
console.print(f"{prefix}{marker} [bold cyan]{titel.nummer}[/bold cyan] {titel.kurztext}{pos_info}")
|
|
275
|
+
for ut in titel.untertitel:
|
|
276
|
+
_drucke(ut, tiefe + 1)
|
|
277
|
+
|
|
278
|
+
console.print()
|
|
279
|
+
console.print(f"[bold]{lv.titel_lv or datei.name}[/bold] [dim]– {len(lv.alle_positionen)} Positionen gesamt[/dim]")
|
|
280
|
+
for t in lv.titel:
|
|
281
|
+
_drucke(t)
|
|
282
|
+
console.print()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ------------------------------------------------------------------ #
|
|
286
|
+
# gaeb validate <datei>
|
|
287
|
+
# ------------------------------------------------------------------ #
|
|
288
|
+
|
|
289
|
+
@app.command()
|
|
290
|
+
def validate(
|
|
291
|
+
datei: Path = typer.Argument(..., help="GAEB-DA-XML-Datei (.x83, .x84, ...)"),
|
|
292
|
+
phase: str | None = typer.Option(None, "--phase", "-p", help="Phase manuell angeben (z.B. 83, 84). Sonst aus Datei ermittelt."),
|
|
293
|
+
):
|
|
294
|
+
"""Validiert eine GAEB-DA-XML-Datei gegen das offizielle XSD-Schema."""
|
|
295
|
+
if not datei.exists():
|
|
296
|
+
err.print(f"Datei nicht gefunden: {datei}")
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
fehler = gaeb.validate(datei, phase=phase)
|
|
301
|
+
except FileNotFoundError as exc:
|
|
302
|
+
err.print(str(exc))
|
|
303
|
+
err.print("XSD-Schemas können von gaeb.de heruntergeladen werden (siehe gaeb/schemas/README.md).")
|
|
304
|
+
raise typer.Exit(2)
|
|
305
|
+
except ValueError as exc:
|
|
306
|
+
err.print(str(exc))
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
except Exception as exc:
|
|
309
|
+
err.print(f"Fehler bei der Validierung: {type(exc).__name__}: {exc}")
|
|
310
|
+
raise typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
if fehler:
|
|
313
|
+
console.print(f"\n[bold red]✗ {len(fehler)} Validierungsfehler in {datei.name}:[/bold red]")
|
|
314
|
+
for f in fehler:
|
|
315
|
+
console.print(f" [red]•[/red] {f}")
|
|
316
|
+
console.print()
|
|
317
|
+
raise typer.Exit(1)
|
|
318
|
+
else:
|
|
319
|
+
console.print(f"\n[bold green]✓ {datei.name} ist XSD-valide.[/bold green]\n")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ------------------------------------------------------------------ #
|
|
323
|
+
# gaeb export <datei> <ausgabe.xlsx>
|
|
324
|
+
# ------------------------------------------------------------------ #
|
|
325
|
+
|
|
326
|
+
@app.command()
|
|
327
|
+
def export(
|
|
328
|
+
datei: Path = typer.Argument(..., help="GAEB-Quelldatei (.d83, .x83, ...)"),
|
|
329
|
+
ausgabe: Path = typer.Argument(..., help="Ziel-Excel-Datei (.xlsx)"),
|
|
330
|
+
langtext: bool = typer.Option(False, "--langtext", "-l", help="Langtext-Spalte einschließen."),
|
|
331
|
+
encoding: str | None = typer.Option(None, "--encoding", "-e", help="Zeichenkodierung (nur GAEB 90)."),
|
|
332
|
+
):
|
|
333
|
+
"""Exportiert ein Leistungsverzeichnis als Excel-Datei (.xlsx)."""
|
|
334
|
+
lv = _read(datei, encoding=encoding)
|
|
335
|
+
try:
|
|
336
|
+
write_excel(lv, ausgabe, langtext=langtext)
|
|
337
|
+
except ImportError as exc:
|
|
338
|
+
err.print(str(exc))
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
except PermissionError:
|
|
341
|
+
err.print(f"Keine Schreibberechtigung: {ausgabe}")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
err.print(f"Fehler beim Excel-Export: {type(exc).__name__}: {exc}")
|
|
345
|
+
raise typer.Exit(1)
|
|
346
|
+
console.print(f"[green]✓[/green] Exportiert: {datei.name} → {ausgabe.name}")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------ #
|
|
350
|
+
# gaeb diff <alt> <neu>
|
|
351
|
+
# ------------------------------------------------------------------ #
|
|
352
|
+
|
|
353
|
+
@app.command()
|
|
354
|
+
def diff(
|
|
355
|
+
alt: Path = typer.Argument(..., help="Ältere GAEB-Datei"),
|
|
356
|
+
neu: Path = typer.Argument(..., help="Neuere GAEB-Datei"),
|
|
357
|
+
encoding: str | None = typer.Option(None, "--encoding", "-e", help="Zeichenkodierung (nur GAEB 90)."),
|
|
358
|
+
):
|
|
359
|
+
"""Vergleicht zwei GAEB-Dateien und zeigt Unterschiede."""
|
|
360
|
+
lv_alt = _read(alt, encoding=encoding)
|
|
361
|
+
lv_neu = _read(neu, encoding=encoding)
|
|
362
|
+
|
|
363
|
+
ergebnis = gaeb.compare(lv_alt, lv_neu)
|
|
364
|
+
|
|
365
|
+
if not ergebnis.has_changes:
|
|
366
|
+
console.print("\n[green]Keine Unterschiede gefunden.[/green]\n")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
console.print()
|
|
370
|
+
|
|
371
|
+
if ergebnis.only_in_old:
|
|
372
|
+
console.print(f"[bold red]Entfernte Positionen ({len(ergebnis.only_in_old)}):[/bold red]")
|
|
373
|
+
for oz in ergebnis.only_in_old:
|
|
374
|
+
console.print(f" [red]−[/red] {oz}")
|
|
375
|
+
console.print()
|
|
376
|
+
|
|
377
|
+
if ergebnis.only_in_new:
|
|
378
|
+
console.print(f"[bold green]Neue Positionen ({len(ergebnis.only_in_new)}):[/bold green]")
|
|
379
|
+
for oz in ergebnis.only_in_new:
|
|
380
|
+
console.print(f" [green]+[/green] {oz}")
|
|
381
|
+
console.print()
|
|
382
|
+
|
|
383
|
+
if ergebnis.title_changes:
|
|
384
|
+
console.print(f"[bold yellow]Titeländerungen ({len(ergebnis.title_changes)}):[/bold yellow]")
|
|
385
|
+
for tc in ergebnis.title_changes:
|
|
386
|
+
if tc.change_type == "removed":
|
|
387
|
+
console.print(f" [red]− Titel {tc.oz}[/red] [dim]{tc.alt_name or ''}[/dim]")
|
|
388
|
+
elif tc.change_type == "added":
|
|
389
|
+
console.print(f" [green]+ Titel {tc.oz}[/green] {tc.neu_name or ''}")
|
|
390
|
+
elif tc.change_type == "name":
|
|
391
|
+
console.print(f" [yellow]~ Titel {tc.oz}[/yellow] [red]{tc.alt_name or ''}[/red] → [green]{tc.neu_name or ''}[/green]")
|
|
392
|
+
elif tc.change_type == "langtext":
|
|
393
|
+
console.print(f" [yellow]~ Titel {tc.oz} Langtext geändert[/yellow]")
|
|
394
|
+
console.print()
|
|
395
|
+
|
|
396
|
+
if ergebnis.changed:
|
|
397
|
+
table = Table(box=box.SIMPLE_HEAD)
|
|
398
|
+
table.add_column("OZ", style="cyan", no_wrap=True)
|
|
399
|
+
table.add_column("Kurztext")
|
|
400
|
+
table.add_column("Feld")
|
|
401
|
+
table.add_column("Alt", style="red")
|
|
402
|
+
table.add_column("Neu", style="green")
|
|
403
|
+
for pa in ergebnis.changed:
|
|
404
|
+
first = True
|
|
405
|
+
for fa in pa.changes:
|
|
406
|
+
table.add_row(
|
|
407
|
+
pa.oz if first else "",
|
|
408
|
+
pa.kurztext[:40] if first else "",
|
|
409
|
+
fa.field,
|
|
410
|
+
_fmt_feldwert(fa.alt),
|
|
411
|
+
_fmt_feldwert(fa.neu),
|
|
412
|
+
)
|
|
413
|
+
first = False
|
|
414
|
+
console.print(f"[bold yellow]Geänderte Positionen ({len(ergebnis.changed)}):[/bold yellow]")
|
|
415
|
+
console.print(table)
|
|
416
|
+
|
|
417
|
+
gs_alt = ergebnis.gesamtsumme_alt
|
|
418
|
+
gs_neu = ergebnis.gesamtsumme_neu
|
|
419
|
+
gs_diff = ergebnis.gesamtsumme_diff
|
|
420
|
+
if gs_alt is not None or gs_neu is not None:
|
|
421
|
+
console.print(f" Gesamtsumme alt: {gs_alt:,.2f}" if gs_alt else " Gesamtsumme alt: —")
|
|
422
|
+
console.print(f" Gesamtsumme neu: {gs_neu:,.2f}" if gs_neu else " Gesamtsumme neu: —")
|
|
423
|
+
if gs_diff is not None:
|
|
424
|
+
farbe = "green" if gs_diff <= 0 else "red"
|
|
425
|
+
console.print(f" Differenz: [{farbe}]{gs_diff:+,.2f}[/{farbe}]")
|
|
426
|
+
console.print()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
app()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gaeb-cli
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Command-line tool for reading, writing, converting and diffing GAEB files (AVA data exchange format)
|
|
5
|
+
Keywords: gaeb,ava,construction,cli
|
|
6
|
+
Author: Attackwave
|
|
7
|
+
Author-email: Attackwave <attackwave@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Office/Business
|
|
14
|
+
Requires-Dist: gaeb>=0.5.2
|
|
15
|
+
Requires-Dist: typer>=0.12
|
|
16
|
+
Requires-Dist: rich>=13.0
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Project-URL: Homepage, https://github.com/Attackwave/gaeb-python
|
|
19
|
+
Project-URL: Repository, https://github.com/Attackwave/gaeb-python
|
|
20
|
+
Project-URL: Issues, https://github.com/Attackwave/gaeb-python/issues
|
|
21
|
+
Project-URL: Changelog, https://github.com/Attackwave/gaeb-python/blob/main/CHANGELOG.md
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# gaeb-cli
|
|
25
|
+
|
|
26
|
+
Kommandozeilen-Tool zum Lesen, Anzeigen, Konvertieren und Vergleichen von **GAEB-Dateien** (AVA – Ausschreibung, Vergabe, Abrechnung im deutschen Bauwesen).
|
|
27
|
+
|
|
28
|
+
Basiert auf der [`gaeb`](../gaeb/)-Bibliothek.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install gaeb-cli
|
|
34
|
+
|
|
35
|
+
# Mit Excel-Export
|
|
36
|
+
pip install gaeb-cli "gaeb[excel]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Befehle
|
|
40
|
+
|
|
41
|
+
### `gaeb info` – Metadaten anzeigen
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
gaeb info ausschreibung.d83
|
|
45
|
+
gaeb info ausschreibung.x83 # zeigt xml_version
|
|
46
|
+
gaeb info ausschreibung.d83 --encoding cp1252
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Gibt Dateiname, Format (inkl. DA XML-Version), Phase, LV-Titel, Projekt, Auftraggeber, Anzahl Positionen und Gesamtsumme aus.
|
|
50
|
+
|
|
51
|
+
### `gaeb show` – Positionen als Tabelle
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
gaeb show ausschreibung.d83
|
|
55
|
+
gaeb show ausschreibung.d83 --langtext # mit Langtext
|
|
56
|
+
gaeb show ausschreibung.d83 --encoding cp850
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `gaeb struktur` – Titelhierarchie
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gaeb struktur ausschreibung.d83
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Gibt die Gliederung des LV als Baum aus (Titel und Untertitel).
|
|
66
|
+
|
|
67
|
+
### `gaeb convert` – Konvertieren
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# GAEB 90 → GAEB DA XML
|
|
71
|
+
gaeb convert ausschreibung.d83 ausschreibung.x83
|
|
72
|
+
|
|
73
|
+
# GAEB DA XML → GAEB 2000
|
|
74
|
+
gaeb convert ausschreibung.x83 ausschreibung.p83
|
|
75
|
+
|
|
76
|
+
# → Excel
|
|
77
|
+
gaeb convert ausschreibung.x83 ausgabe.xlsx
|
|
78
|
+
|
|
79
|
+
# Phase wechseln
|
|
80
|
+
gaeb convert anfrage.d83 angebot.d84
|
|
81
|
+
|
|
82
|
+
# Phase explizit angeben
|
|
83
|
+
gaeb convert ausschreibung.d83 ausgabe.x84 --phase 84
|
|
84
|
+
|
|
85
|
+
# Encoding angeben (nur GAEB 90 / GAEB 2000)
|
|
86
|
+
gaeb convert quelle.d83 ziel.d83 --lese-encoding cp1252 --schreib-encoding cp850
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Zielformat wird aus der Dateiendung abgeleitet:
|
|
90
|
+
|
|
91
|
+
| Endung | Format |
|
|
92
|
+
|--------|--------|
|
|
93
|
+
| `.d8x`, `.d87`–`.d89` | GAEB 90 |
|
|
94
|
+
| `.p8x` | GAEB 2000 |
|
|
95
|
+
| `.x31`, `.x50`–`.x52`, `.x8x`, `.x93`–`.x97` | GAEB DA XML |
|
|
96
|
+
| `.xlsx` | Excel |
|
|
97
|
+
|
|
98
|
+
### `gaeb export` – Excel-Export
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
gaeb export ausschreibung.x83 ausgabe.xlsx
|
|
102
|
+
gaeb export ausschreibung.d83 ausgabe.xlsx --langtext
|
|
103
|
+
gaeb export ausschreibung.d83 ausgabe.xlsx --encoding cp850
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Exportiert ein LV als strukturierte Excel-Datei mit Deckblatt und Positionstabelle.
|
|
107
|
+
Erfordert `pip install gaeb[excel]`.
|
|
108
|
+
|
|
109
|
+
### `gaeb diff` – Zwei LVs vergleichen
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
gaeb diff ausschreibung.x83 angebot.x84
|
|
113
|
+
gaeb diff alt.d83 neu.d83 --encoding cp850
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Zeigt gelöschte, neue und geänderte Positionen mit Feldvergleich und finanzieller Zusammenfassung.
|
|
117
|
+
|
|
118
|
+
### `gaeb validate` – XSD-Validierung
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
gaeb validate ausschreibung.x83
|
|
122
|
+
gaeb validate ausschreibung.x83 --phase 83
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Validiert eine GAEB-DA-XML-Datei gegen das offizielle XSD-Schema.
|
|
126
|
+
|
|
127
|
+
> **Hinweis:** Die XSD-Schemas müssen separat heruntergeladen werden.
|
|
128
|
+
> Siehe [`gaeb/schemas/README.md`](../gaeb/schemas/README.md).
|
|
129
|
+
|
|
130
|
+
## Unterstützte Phasen
|
|
131
|
+
|
|
132
|
+
| Bereich | Codes |
|
|
133
|
+
|---------|-------|
|
|
134
|
+
| Mengenermittlung | 31 |
|
|
135
|
+
| Kostenermittlung | 50, 51, 52 |
|
|
136
|
+
| Vergabe (Procurement) | 80–89, 83Z, 84Z, 86ZR, 86ZE |
|
|
137
|
+
| Handel / Handwerk | 93–97 |
|
|
138
|
+
|
|
139
|
+
## Lizenz
|
|
140
|
+
|
|
141
|
+
MIT – siehe [LICENSE](../LICENSE)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
gaeb_cli/__init__.py,sha256=97xBNXgQC35T36yKFmjm7ixxWtW9akVoUdDmv9QTffA,76
|
|
2
|
+
gaeb_cli/main.py,sha256=afQ4ZMWGNfX7WBIFR4n8anGHN5ruw3nEC7cgmC4cH_A,16893
|
|
3
|
+
gaeb_cli-0.5.2.dist-info/WHEEL,sha256=wGHOIGFfyqJ_mvPzL8rbdQzrAWETnDIh93qRV-Ucyms,80
|
|
4
|
+
gaeb_cli-0.5.2.dist-info/entry_points.txt,sha256=2ZyNnPTcMRXicjPtxA1Xvbrg606c6UPmXqGSiwGvSo0,44
|
|
5
|
+
gaeb_cli-0.5.2.dist-info/METADATA,sha256=8FcNqJmnh_Vzanuq92F1VGPDjEIXari7EYz4TWtVnSU,3847
|
|
6
|
+
gaeb_cli-0.5.2.dist-info/RECORD,,
|