lxw-cli 0.1.0__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.
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from lxw_cli.commands._common import ARCHIVED_HINT, load_json_arg, state
8
+ from lxw_cli.core import services
9
+ from lxw_cli.core.constants import DEFAULT_VOUCHER_STATUSES
10
+ from lxw_cli.output import print_count, render, working, write_binary
11
+
12
+ app = typer.Typer(no_args_is_help=True)
13
+
14
+ LIST_COLUMNS = [
15
+ "id",
16
+ "voucherNumber",
17
+ "voucherDate",
18
+ "voucherStatus",
19
+ "contactName",
20
+ "totalAmount",
21
+ "currency",
22
+ ]
23
+
24
+
25
+ @app.command(
26
+ "list",
27
+ epilog="""\
28
+ [bold cyan]Beispiele[/bold cyan]
29
+
30
+ Offene Rechnungen: [green]lxw invoices list --status open[/green]
31
+
32
+ Alle als CSV: [green]lxw --csv invoices list --all[/green]
33
+
34
+ Nach Rechnungsnummer: [green]lxw invoices list --number FB2600682[/green]
35
+
36
+ Inkl. archivierte: [green]lxw invoices list --include-archived[/green]
37
+ """,
38
+ )
39
+ def list_invoices(
40
+ ctx: typer.Context,
41
+ limit: int = typer.Option(
42
+ 25, "--limit", "-n", help="Maximale Anzahl Einträge (0 = alle)."
43
+ ),
44
+ fetch_all: bool = typer.Option(
45
+ False, "--all", "-a", help="Alle Treffer laden (überschreibt --limit)."
46
+ ),
47
+ voucher_status: str = typer.Option(
48
+ DEFAULT_VOUCHER_STATUSES,
49
+ "--status",
50
+ help="Komma-separiert: draft,open,paid,paidoff,voided,overdue,accepted,rejected.",
51
+ ),
52
+ voucher_number: str | None = typer.Option(
53
+ None, "--number", help="Filter: exakte Rechnungsnummer."
54
+ ),
55
+ contact_id: str | None = typer.Option(
56
+ None, "--contact-id", help="Filter: Kontakt-UUID (Kunde)."
57
+ ),
58
+ include_archived: bool = typer.Option(
59
+ False,
60
+ "--include-archived",
61
+ help="Auch archivierte Rechnungen anzeigen (Standard: ausgeblendet).",
62
+ ),
63
+ ) -> None:
64
+ """Rechnungen auflisten.
65
+
66
+ Listet Rechnungen aus /v1/voucherlist (salesinvoice, invoice,
67
+ downpaymentinvoice). Standardmäßig werden bis zu 25 Einträge gezeigt — mit
68
+ --all bzw. --limit 0 werden alle Seiten geladen. Filtern lässt sich nach
69
+ Status, exakter Rechnungsnummer und Kunde.
70
+
71
+ Archivierte Rechnungen sind standardmäßig ausgeblendet; --include-archived
72
+ zeigt sie. Am Ende wird die Gesamtzahl ausgegeben. Tipp: get/pdf akzeptieren
73
+ UUID oder Rechnungsnummer (z.B. FB2600682).
74
+ """
75
+ s = state(ctx)
76
+ with working("Lade Rechnungen …"):
77
+ result = services.list_invoices(
78
+ s.client,
79
+ status=voucher_status,
80
+ number=voucher_number,
81
+ contact_id=contact_id,
82
+ include_archived=include_archived,
83
+ limit=limit,
84
+ fetch_all=fetch_all,
85
+ )
86
+ render(result.items, s.output_format, columns=LIST_COLUMNS, output_path=s.output_path)
87
+ note = None if include_archived else ARCHIVED_HINT
88
+ print_count(len(result.items), result.total, noun="Rechnungen", note=note)
89
+
90
+
91
+ @app.command(
92
+ "get",
93
+ epilog="""\
94
+ [bold cyan]Beispiele[/bold cyan]
95
+
96
+ Nach Rechnungsnummer: [green]lxw invoices get FB2600682[/green]
97
+
98
+ Betrag via jq: [green]lxw --json invoices get FB2600682 | jq .totalGrossAmount[/green]
99
+ """,
100
+ )
101
+ def get_invoice(
102
+ ctx: typer.Context,
103
+ invoice: str = typer.Argument(..., help="UUID oder Rechnungsnummer (z.B. FB2600682)."),
104
+ ) -> None:
105
+ """Eine Rechnung im Detail abrufen.
106
+
107
+ Akzeptiert die UUID oder die Rechnungsnummer (z.B. FB2600682) — die Nummer
108
+ wird automatisch aufgelöst. Mit --json gibt es die vollständigen Rohdaten
109
+ für die Weiterverarbeitung.
110
+ """
111
+ s = state(ctx)
112
+ with working("Lade Rechnung …"):
113
+ data = services.get_invoice(s.client, invoice)
114
+ render(data, s.output_format, output_path=s.output_path)
115
+
116
+
117
+ @app.command(
118
+ "pdf",
119
+ epilog="""\
120
+ [bold cyan]Beispiele[/bold cyan]
121
+
122
+ Ins aktuelle Verzeichnis: [green]lxw invoices pdf FB2600682[/green]
123
+
124
+ In ein Verzeichnis (Auto-Name): [green]lxw invoices pdf FB2600682 -o ~/Rechnungen[/green]
125
+
126
+ Mit festem Dateinamen: [green]lxw invoices pdf FB2600682 -o rechnung.pdf[/green]
127
+ """,
128
+ )
129
+ def download_pdf(
130
+ ctx: typer.Context,
131
+ invoice: str = typer.Argument(..., help="UUID oder Rechnungsnummer."),
132
+ output: Path | None = typer.Option(
133
+ None,
134
+ "--output",
135
+ "-o",
136
+ help="Ziel-Datei oder -Verzeichnis (Standard: aktuelles Verzeichnis).",
137
+ ),
138
+ ) -> None:
139
+ """PDF einer Rechnung herunterladen.
140
+
141
+ Akzeptiert UUID oder Rechnungsnummer. Ohne -o landet die Datei im aktuellen
142
+ Verzeichnis (z.B. invoice-FB2600682.pdf); zeigt -o auf ein Verzeichnis, wird
143
+ der Dateiname automatisch vergeben. Hinweis: Entwürfe haben noch kein PDF.
144
+ """
145
+ s = state(ctx)
146
+ with working("Lade PDF …"):
147
+ data = services.download_invoice_pdf(s.client, invoice)
148
+ write_binary(data, output, default_name=f"invoice-{invoice}.pdf")
149
+
150
+
151
+ @app.command(
152
+ "create-draft",
153
+ epilog="""\
154
+ [bold cyan]Beispiele[/bold cyan]
155
+
156
+ Aus einer Datei: [green]lxw invoices create-draft --body @rechnung.json[/green]
157
+
158
+ Inline: [green]lxw invoices create-draft --body '{…}'[/green]
159
+ """,
160
+ )
161
+ def create_draft(
162
+ ctx: typer.Context,
163
+ body: str = typer.Option(
164
+ ...,
165
+ "--body",
166
+ help="JSON-Body inline oder @datei.json (siehe API-Doku).",
167
+ ),
168
+ ) -> None:
169
+ """Neue Rechnung als Draft anlegen (kein Finalize).
170
+
171
+ Erwartet den vollständigen JSON-Body der Lexware-API — inline oder als
172
+ @pfad.json. Die Rechnung wird als Entwurf gespeichert (nicht finalisiert).
173
+ Schema siehe https://developers.lexware.io/docs/.
174
+ """
175
+ s = state(ctx)
176
+ payload = load_json_arg(body)
177
+ with working("Lege Rechnung an …"):
178
+ result = services.create_invoice(s.client, payload)
179
+ render(result, s.output_format, output_path=s.output_path)
@@ -0,0 +1,167 @@
1
+ """Subcommand `lxw mcp …` — manages Claude Code/Desktop integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ from dotenv import dotenv_values
12
+
13
+ from lxw_cli.config import ENV_KEY, global_env_path, load_config, store_key
14
+ from lxw_cli.output import console, err_console
15
+
16
+ app = typer.Typer(no_args_is_help=True)
17
+
18
+ MCP_NAME = "lexware"
19
+
20
+
21
+ def _check_claude() -> None:
22
+ if not shutil.which("claude"):
23
+ err_console.print(
24
+ "[red]Fehler:[/red] `claude` CLI nicht gefunden. "
25
+ "Installiere zuerst Claude Code (https://claude.com/claude-code)."
26
+ )
27
+ raise typer.Exit(code=2)
28
+
29
+
30
+ def _mcp_command() -> list[str]:
31
+ """Build the command Claude will use to launch the MCP server.
32
+
33
+ Prefer the `lxw-mcp` console script when it's on PATH (works for
34
+ pipx-installed setups). Otherwise fall back to the current interpreter
35
+ plus `-m lxw_cli.mcp_server` so the venv path is captured even when
36
+ `.venv/bin` isn't on the system PATH.
37
+ """
38
+ on_path = shutil.which("lxw-mcp")
39
+ if on_path:
40
+ return [on_path]
41
+ # Look for the entry-point next to the running python (typical venv layout;
42
+ # on Windows console scripts get an .exe suffix)
43
+ exe_suffix = ".exe" if sys.platform == "win32" else ""
44
+ candidate = Path(sys.executable).parent / f"lxw-mcp{exe_suffix}"
45
+ if candidate.exists():
46
+ return [str(candidate)]
47
+ return [sys.executable, "-m", "lxw_cli.mcp_server"]
48
+
49
+
50
+ def _ensure_global_key(api_key: str) -> None:
51
+ """Make sure the MCP server can resolve the key from the global config.
52
+
53
+ The key is deliberately NOT passed via `claude mcp add -e …` — that would
54
+ expose it in the process list and store a second plaintext copy in the
55
+ Claude config. The server resolves it itself from the global `.env`
56
+ (chmod 600), so we only have to guarantee it exists there.
57
+ """
58
+ path = global_env_path()
59
+ existing = (dotenv_values(path).get(ENV_KEY) or "").strip() if path.is_file() else ""
60
+ if not existing:
61
+ stored = store_key(api_key)
62
+ err_console.print(
63
+ f"[dim]API-Key in {stored} hinterlegt (wird vom MCP-Server gelesen).[/dim]"
64
+ )
65
+ elif existing != api_key:
66
+ err_console.print(
67
+ f"[yellow]Hinweis:[/yellow] {path} enthält einen anderen API-Key — "
68
+ "der MCP-Server verwendet den dort hinterlegten."
69
+ )
70
+
71
+
72
+ @app.command("install-claude")
73
+ def install_claude(
74
+ scope: str = typer.Option(
75
+ "user", "--scope", help="user (global), project (per repo), oder local."
76
+ ),
77
+ force: bool = typer.Option(
78
+ False, "--force", help="Vorher entfernen, falls bereits registriert."
79
+ ),
80
+ ) -> None:
81
+ """Registriert den Lexware-MCP-Server bei Claude Code."""
82
+ _check_claude()
83
+ config = load_config() # validates LEXWARE_API_KEY
84
+ _ensure_global_key(config.api_key)
85
+
86
+ server_cmd = _mcp_command()
87
+
88
+ if force:
89
+ subprocess.run(
90
+ ["claude", "mcp", "remove", MCP_NAME, "--scope", scope],
91
+ check=False,
92
+ capture_output=True,
93
+ )
94
+
95
+ cmd = [
96
+ "claude",
97
+ "mcp",
98
+ "add",
99
+ MCP_NAME,
100
+ "--scope",
101
+ scope,
102
+ "--",
103
+ *server_cmd,
104
+ ]
105
+ result = subprocess.run(cmd, capture_output=True, text=True)
106
+ if result.returncode != 0:
107
+ err_console.print(f"[red]claude mcp add fehlgeschlagen:[/red]\n{result.stderr}")
108
+ if "already exists" in (result.stderr or "").lower():
109
+ err_console.print(
110
+ "[yellow]Hinweis:[/yellow] Mit `--force` kannst du die "
111
+ "bestehende Registrierung überschreiben."
112
+ )
113
+ raise typer.Exit(code=result.returncode)
114
+ console.print(
115
+ f"[green]✓[/green] Lexware MCP-Server bei Claude Code registriert "
116
+ f"(scope={scope})."
117
+ )
118
+ console.print(
119
+ "Test: Öffne Claude Code und frag z.B. "
120
+ "[bold]\"Wie viele offene Rechnungen habe ich?\"[/bold]"
121
+ )
122
+
123
+
124
+ @app.command("uninstall-claude")
125
+ def uninstall_claude(
126
+ scope: str = typer.Option("user", "--scope", help="Gleicher Scope wie beim Install."),
127
+ ) -> None:
128
+ """Entfernt den Lexware-MCP-Server aus Claude Code."""
129
+ _check_claude()
130
+ result = subprocess.run(
131
+ ["claude", "mcp", "remove", MCP_NAME, "--scope", scope],
132
+ capture_output=True,
133
+ text=True,
134
+ )
135
+ if result.returncode != 0:
136
+ err_console.print(f"[red]Fehler:[/red] {result.stderr}")
137
+ raise typer.Exit(code=result.returncode)
138
+ console.print(f"[green]✓[/green] Lexware MCP-Server entfernt (scope={scope}).")
139
+
140
+
141
+ @app.command("status")
142
+ def status() -> None:
143
+ """Zeigt, ob der Lexware-MCP-Server aktuell bei Claude Code registriert ist."""
144
+ _check_claude()
145
+ result = subprocess.run(
146
+ ["claude", "mcp", "list"], capture_output=True, text=True
147
+ )
148
+ if result.returncode != 0:
149
+ err_console.print(f"[red]Fehler:[/red] {result.stderr}")
150
+ raise typer.Exit(code=result.returncode)
151
+ if MCP_NAME in result.stdout:
152
+ for line in result.stdout.splitlines():
153
+ if MCP_NAME in line:
154
+ console.print(f"[green]✓[/green] {line.strip()}")
155
+ else:
156
+ console.print(
157
+ "[yellow]✗[/yellow] Lexware ist nicht registriert. "
158
+ "Setup: [bold]lxw mcp install-claude[/bold]"
159
+ )
160
+
161
+
162
+ @app.command("serve")
163
+ def serve() -> None:
164
+ """Startet den MCP-Server (intern von Claude Code aufgerufen — selten manuell nötig)."""
165
+ from lxw_cli.mcp_server import run as run_server
166
+
167
+ run_server()
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from lxw_cli.commands._common import ARCHIVED_HINT, load_json_arg, state
8
+ from lxw_cli.core import services
9
+ from lxw_cli.core.constants import DEFAULT_VOUCHER_STATUSES
10
+ from lxw_cli.output import print_count, render, working, write_binary
11
+
12
+ app = typer.Typer(no_args_is_help=True)
13
+
14
+ LIST_COLUMNS = [
15
+ "id",
16
+ "voucherNumber",
17
+ "voucherDate",
18
+ "voucherStatus",
19
+ "contactName",
20
+ "totalAmount",
21
+ "currency",
22
+ ]
23
+
24
+
25
+ @app.command(
26
+ "list",
27
+ epilog="""\
28
+ [bold cyan]Beispiele[/bold cyan]
29
+
30
+ Offene Aufträge: [green]lxw orders list --status open[/green]
31
+
32
+ Alle als CSV: [green]lxw --csv -o auftraege.csv orders list --all[/green]
33
+
34
+ Inkl. archivierte: [green]lxw orders list --include-archived[/green]
35
+ """,
36
+ )
37
+ def list_orders(
38
+ ctx: typer.Context,
39
+ limit: int = typer.Option(
40
+ 25, "--limit", "-n", help="Maximale Anzahl Einträge (0 = alle)."
41
+ ),
42
+ fetch_all: bool = typer.Option(
43
+ False, "--all", "-a", help="Alle Treffer laden (überschreibt --limit)."
44
+ ),
45
+ voucher_status: str = typer.Option(
46
+ DEFAULT_VOUCHER_STATUSES,
47
+ "--status",
48
+ help="Komma-separiert (z.B. draft,open,accepted,rejected).",
49
+ ),
50
+ include_archived: bool = typer.Option(
51
+ False,
52
+ "--include-archived",
53
+ help="Auch archivierte Aufträge anzeigen (Standard: ausgeblendet).",
54
+ ),
55
+ ) -> None:
56
+ """Aufträge (Auftragsbestätigungen) auflisten (über /v1/voucherlist).
57
+
58
+ Standardmäßig bis zu 25 Aufträge (--all bzw. --limit 0 lädt alle),
59
+ filterbar nach Status. Archivierte Aufträge sind standardmäßig
60
+ ausgeblendet; --include-archived zeigt sie. get/pdf akzeptieren UUID oder
61
+ Auftragsnummer.
62
+ """
63
+ s = state(ctx)
64
+ with working("Lade Aufträge …"):
65
+ result = services.list_order_confirmations(
66
+ s.client,
67
+ status=voucher_status,
68
+ include_archived=include_archived,
69
+ limit=limit,
70
+ fetch_all=fetch_all,
71
+ )
72
+ render(result.items, s.output_format, columns=LIST_COLUMNS, output_path=s.output_path)
73
+ note = None if include_archived else ARCHIVED_HINT
74
+ print_count(len(result.items), result.total, noun="Aufträge", note=note)
75
+
76
+
77
+ @app.command("get")
78
+ def get_order(
79
+ ctx: typer.Context,
80
+ order: str = typer.Argument(..., help="UUID oder Auftragsnummer."),
81
+ ) -> None:
82
+ """Einen Auftrag im Detail abrufen. Akzeptiert UUID oder Auftragsnummer."""
83
+ s = state(ctx)
84
+ with working("Lade Auftrag …"):
85
+ data = services.get_order_confirmation(s.client, order)
86
+ render(data, s.output_format, output_path=s.output_path)
87
+
88
+
89
+ @app.command("pdf")
90
+ def download_pdf(
91
+ ctx: typer.Context,
92
+ order: str = typer.Argument(..., help="UUID oder Auftragsnummer."),
93
+ output: Path | None = typer.Option(
94
+ None,
95
+ "--output",
96
+ "-o",
97
+ help="Ziel-Datei oder -Verzeichnis (Standard: aktuelles Verzeichnis).",
98
+ ),
99
+ ) -> None:
100
+ """PDF eines Auftrags herunterladen. Akzeptiert UUID oder Auftragsnummer."""
101
+ s = state(ctx)
102
+ with working("Lade PDF …"):
103
+ data = services.download_order_confirmation_pdf(s.client, order)
104
+ write_binary(data, output, default_name=f"order-{order}.pdf")
105
+
106
+
107
+ @app.command("create-draft")
108
+ def create_draft(
109
+ ctx: typer.Context,
110
+ body: str = typer.Option(..., "--body", help="JSON-Body inline oder @datei.json."),
111
+ ) -> None:
112
+ """Neuen Auftrag als Draft anlegen."""
113
+ s = state(ctx)
114
+ payload = load_json_arg(body)
115
+ with working("Lege Auftrag an …"):
116
+ result = services.create_order_confirmation(s.client, payload)
117
+ render(result, s.output_format, output_path=s.output_path)
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from lxw_cli.commands._common import ARCHIVED_HINT, load_json_arg, state
8
+ from lxw_cli.core import services
9
+ from lxw_cli.core.constants import DEFAULT_VOUCHER_STATUSES
10
+ from lxw_cli.output import print_count, render, working, write_binary
11
+
12
+ app = typer.Typer(no_args_is_help=True)
13
+
14
+ LIST_COLUMNS = [
15
+ "id",
16
+ "voucherNumber",
17
+ "voucherDate",
18
+ "voucherStatus",
19
+ "contactName",
20
+ "totalAmount",
21
+ "currency",
22
+ ]
23
+
24
+
25
+ @app.command(
26
+ "list",
27
+ epilog="""\
28
+ [bold cyan]Beispiele[/bold cyan]
29
+
30
+ Offene Angebote: [green]lxw quotations list --status open[/green]
31
+
32
+ Alle als CSV: [green]lxw --csv -o angebote.csv quotations list --all[/green]
33
+
34
+ Inkl. archivierte: [green]lxw quotations list --include-archived[/green]
35
+ """,
36
+ )
37
+ def list_quotations(
38
+ ctx: typer.Context,
39
+ limit: int = typer.Option(
40
+ 25, "--limit", "-n", help="Maximale Anzahl Einträge (0 = alle)."
41
+ ),
42
+ fetch_all: bool = typer.Option(
43
+ False, "--all", "-a", help="Alle Treffer laden (überschreibt --limit)."
44
+ ),
45
+ voucher_status: str = typer.Option(
46
+ DEFAULT_VOUCHER_STATUSES,
47
+ "--status",
48
+ help="Komma-separiert (z.B. draft,open,accepted,rejected).",
49
+ ),
50
+ include_archived: bool = typer.Option(
51
+ False,
52
+ "--include-archived",
53
+ help="Auch archivierte Angebote anzeigen (Standard: ausgeblendet).",
54
+ ),
55
+ ) -> None:
56
+ """Angebote auflisten (über /v1/voucherlist).
57
+
58
+ Standardmäßig bis zu 25 Angebote (--all bzw. --limit 0 lädt alle),
59
+ filterbar nach Status. Archivierte Angebote sind standardmäßig
60
+ ausgeblendet; --include-archived zeigt sie. get/pdf akzeptieren UUID oder
61
+ Angebotsnummer (z.B. AG5241415).
62
+ """
63
+ s = state(ctx)
64
+ with working("Lade Angebote …"):
65
+ result = services.list_quotations(
66
+ s.client,
67
+ status=voucher_status,
68
+ include_archived=include_archived,
69
+ limit=limit,
70
+ fetch_all=fetch_all,
71
+ )
72
+ render(result.items, s.output_format, columns=LIST_COLUMNS, output_path=s.output_path)
73
+ note = None if include_archived else ARCHIVED_HINT
74
+ print_count(len(result.items), result.total, noun="Angebote", note=note)
75
+
76
+
77
+ @app.command("get")
78
+ def get_quotation(
79
+ ctx: typer.Context,
80
+ quotation: str = typer.Argument(..., help="UUID oder Angebotsnummer."),
81
+ ) -> None:
82
+ """Ein Angebot im Detail abrufen. Akzeptiert UUID oder Angebotsnummer."""
83
+ s = state(ctx)
84
+ with working("Lade Angebot …"):
85
+ data = services.get_quotation(s.client, quotation)
86
+ render(data, s.output_format, output_path=s.output_path)
87
+
88
+
89
+ @app.command("pdf")
90
+ def download_pdf(
91
+ ctx: typer.Context,
92
+ quotation: str = typer.Argument(..., help="UUID oder Angebotsnummer."),
93
+ output: Path | None = typer.Option(
94
+ None,
95
+ "--output",
96
+ "-o",
97
+ help="Ziel-Datei oder -Verzeichnis (Standard: aktuelles Verzeichnis).",
98
+ ),
99
+ ) -> None:
100
+ """PDF eines Angebots herunterladen. Akzeptiert UUID oder Angebotsnummer."""
101
+ s = state(ctx)
102
+ with working("Lade PDF …"):
103
+ data = services.download_quotation_pdf(s.client, quotation)
104
+ write_binary(data, output, default_name=f"quotation-{quotation}.pdf")
105
+
106
+
107
+ @app.command("create-draft")
108
+ def create_draft(
109
+ ctx: typer.Context,
110
+ body: str = typer.Option(..., "--body", help="JSON-Body inline oder @datei.json."),
111
+ ) -> None:
112
+ """Neues Angebot als Draft anlegen."""
113
+ s = state(ctx)
114
+ payload = load_json_arg(body)
115
+ with working("Lege Angebot an …"):
116
+ result = services.create_quotation(s.client, payload)
117
+ render(result, s.output_format, output_path=s.output_path)
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from lxw_cli.commands._common import ARCHIVED_HINT, load_json_arg, state
6
+ from lxw_cli.core import services
7
+ from lxw_cli.core.constants import DEFAULT_VOUCHER_STATUSES, DEFAULT_VOUCHER_TYPES
8
+ from lxw_cli.output import print_count, render, working
9
+
10
+ app = typer.Typer(no_args_is_help=True)
11
+
12
+ LIST_COLUMNS = [
13
+ "id",
14
+ "voucherType",
15
+ "voucherStatus",
16
+ "voucherNumber",
17
+ "voucherDate",
18
+ "contactName",
19
+ "totalAmount",
20
+ "currency",
21
+ ]
22
+
23
+
24
+ @app.command(
25
+ "list",
26
+ epilog="""\
27
+ [bold cyan]Beispiele[/bold cyan]
28
+
29
+ Nur Eingangsrechnungen: [green]lxw vouchers list --type purchaseinvoice[/green]
30
+
31
+ Offene & überfällige: [green]lxw vouchers list --status open,overdue[/green]
32
+
33
+ Alle eines Kontakts: [green]lxw vouchers list --contact-id <uuid> --all[/green]
34
+
35
+ Inkl. archivierte: [green]lxw vouchers list --include-archived[/green]
36
+ """,
37
+ )
38
+ def list_vouchers(
39
+ ctx: typer.Context,
40
+ limit: int = typer.Option(
41
+ 25, "--limit", "-n", help="Maximale Anzahl Einträge (0 = alle)."
42
+ ),
43
+ fetch_all: bool = typer.Option(
44
+ False, "--all", "-a", help="Alle Treffer laden (überschreibt --limit)."
45
+ ),
46
+ voucher_type: str = typer.Option(
47
+ DEFAULT_VOUCHER_TYPES,
48
+ "--type",
49
+ help=(
50
+ "Komma-separiert: salesinvoice,salescreditnote,purchaseinvoice,"
51
+ "purchasecreditnote,invoice,downpaymentinvoice,creditnote,"
52
+ "orderconfirmation,quotation,deliverynote."
53
+ ),
54
+ ),
55
+ voucher_status: str = typer.Option(
56
+ DEFAULT_VOUCHER_STATUSES,
57
+ "--status",
58
+ help="Komma-separiert (z.B. open,paid,paidoff).",
59
+ ),
60
+ voucher_number: str | None = typer.Option(
61
+ None, "--number", help="Filter: exakte Belegnummer."
62
+ ),
63
+ contact_id: str | None = typer.Option(
64
+ None, "--contact-id", help="Filter: Kontakt-UUID."
65
+ ),
66
+ include_archived: bool = typer.Option(
67
+ False,
68
+ "--include-archived",
69
+ help="Auch archivierte Belege anzeigen (Standard: ausgeblendet).",
70
+ ),
71
+ ) -> None:
72
+ """Belege auflisten (alle voucher-types).
73
+
74
+ Anders als 'invoices' deckt dieser Befehl alle Beleg-Typen ab — von
75
+ salesinvoice über purchaseinvoice bis creditnote (siehe --type).
76
+ Standardmäßig bis zu 25 Einträge (--all lädt alle), filterbar nach Typ,
77
+ Status, exakter Belegnummer und Kontakt.
78
+
79
+ Archivierte Belege sind standardmäßig ausgeblendet; --include-archived
80
+ zeigt sie. Am Ende wird die Gesamtzahl ausgegeben.
81
+ """
82
+ s = state(ctx)
83
+ with working("Lade Belege …"):
84
+ result = services.list_vouchers(
85
+ s.client,
86
+ voucher_type=voucher_type,
87
+ status=voucher_status,
88
+ number=voucher_number,
89
+ contact_id=contact_id,
90
+ include_archived=include_archived,
91
+ limit=limit,
92
+ fetch_all=fetch_all,
93
+ )
94
+ render(result.items, s.output_format, columns=LIST_COLUMNS, output_path=s.output_path)
95
+ note = None if include_archived else ARCHIVED_HINT
96
+ print_count(len(result.items), result.total, noun="Belege", note=note)
97
+
98
+
99
+ @app.command("get")
100
+ def get_voucher(
101
+ ctx: typer.Context,
102
+ voucher: str = typer.Argument(..., help="UUID oder Belegnummer."),
103
+ ) -> None:
104
+ """Einen Beleg im Detail abrufen. Akzeptiert UUID oder Belegnummer."""
105
+ s = state(ctx)
106
+ with working("Lade Beleg …"):
107
+ data = services.get_voucher(s.client, voucher)
108
+ render(data, s.output_format, output_path=s.output_path)
109
+
110
+
111
+ @app.command("create-draft")
112
+ def create_draft(
113
+ ctx: typer.Context,
114
+ body: str = typer.Option(..., "--body", help="JSON-Body inline oder @datei.json."),
115
+ ) -> None:
116
+ """Neuen Beleg anlegen."""
117
+ s = state(ctx)
118
+ payload = load_json_arg(body)
119
+ with working("Lege Beleg an …"):
120
+ result = services.create_voucher(s.client, payload)
121
+ render(result, s.output_format, output_path=s.output_path)