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 ADDED
@@ -0,0 +1,3 @@
1
+ """GAEB CLI – Command-line tool for GAEB files."""
2
+
3
+ __version__ = "0.5.2"
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ gaeb = gaeb_cli.main:app
3
+