diff --git a/Documentation.md b/Documentation.md index d243d9f..290f485 100644 --- a/Documentation.md +++ b/Documentation.md @@ -291,3 +291,59 @@ Checkout-Updater nicht. Technische Grundlagen: [pip Versionsauflösung und Installationsbericht](https://pip.pypa.io/en/stable/cli/pip_install/), [Python venv und feste Umgebungspfade](https://docs.python.org/3/library/venv.html). Umgebungen bleiben deshalb an ihrem Erstellungsort; nur der Zeiger wird gewechselt. + + +## Konsolenbefehle installieren + +Einmal im Checkout `python3 scripts/install_commands.py` ausführen. Der Installer +benötigt kein sudo und verlinkt `einvoice` sowie `jr-einvoice` nach `~/.local/bin`. +Die verlinkten Starter finden den Checkout auch beim Aufruf aus einem anderen +Verzeichnis und verwenden den aktiven Update-/Rollback-Stand. Beide Befehle werden +mit `--help` geprüft. Bei Fehlern setzt der Installer seine Änderungen zurück. +Vorhandene andere Dateien/Symlinks werden unter `*.before-einvoice-` +gesichert; bereits korrekt eingerichtete Links bleiben erhalten. + +```bash +# Einmalig: +cd /home/johannes/localdev/einvoice +python3 scripts/install_commands.py + +# Danach auch außerhalb des Checkouts: +einvoice --help +einvoice --customers +einvoice --update --status +einvoice --update --apply +einvoice --update --rollback +jr-einvoice --help +``` + +Abweichender Zielordner: `--bin-dir /absoluter/pfad`. Falls dieser Ordner nicht im +PATH steht, zeigt der Installer die passende Export-Zeile an; dauerhaft in die eigene +Shell-Konfiguration übernehmen. Auf Johannes' Rechner ist `~/.local/bin` bereits im +PATH, daher ist keine Änderung der Shell-Konfiguration nötig. Bei einem alten +Shell-Cache `rehash` (zsh) oder `hash -r` (bash) verwenden. Eine aktivierte `.venv` +kann den alten Einstieg vorziehen; mit `deactivate` die verwalteten Befehle nutzen. +Der Checkout muss am Installationsort erhalten bleiben. Nach Verschieben den +Installer am neuen Ort erneut ausführen. Die Starter benötigen bash, readlink und +Python 3; diese sind auf dem vorgesehenen Linux-System vorhanden. + +## Office-Unabhängigkeit und LibreOffice + +Die Quell-, Vorlagen- und Datenformate der Anwendung sind YAML, HTML/Jinja2, SVG +und Markdown. Erzeugt werden PDF/A-3, strukturiertes XML, Prüfberichte, JSON-Manifeste +und optional EML. Weder DOC/DOCX, XLS/XLSX, PPT/PPTX noch COM-/Microsoft-Office-APIs +werden verwendet. Es gibt keine Abhängigkeit von Word, Excel, Outlook oder einer +Microsoft-365-Lizenz. Die Python-Abhängigkeiten und die externen Werkzeuge +WeasyPrint/Mustang/Java benötigen keine Office-Installation. + +Für diese Rechnungslösung muss deshalb beim Wechsel zu LibreOffice nichts nach +ODT/ODS konvertiert werden. ODF-Import/-Export ist keine Funktion des Tools; +Dokumentvorlagen werden weiterhin als HTML/Jinja2 bearbeitet, Daten als YAML. +Die versandfertige E-Rechnung ist das validierte PDF mit eingebettetem XML. +Dieses Original nicht in Office/LibreOffice bearbeiten oder als neues PDF exportieren, +da XML-Einbettung und PDF/A-Eigenschaften dabei verloren gehen können. + +Der optionale EML-Entwurf ist eine Datei ohne Outlook-Automation. Mailprogramm, +Mailkonto, Zugang und tatsächlicher Versand liegen außerhalb der Toolchain. +Die Prüfung betrifft dieses eInvoice-Projekt; andere persönliche oder betriebliche +Office-Dateien und Microsoft-365-Dienste wurden damit nicht geprüft oder migriert. diff --git a/Makefile b/Makefile index a89c526..e0d5b51 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,7 @@ rollback: python3 scripts/update.py --rollback update-status: python3 scripts/update.py --status + +.PHONY: install-commands +install-commands: + python3 scripts/install_commands.py diff --git a/README.md b/README.md index 84da1bd..9230439 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Auf diesem Rechner sind Python-Umgebung, Absenderdaten und Mustang eingerichtet. ```bash cd /home/johannes/localdev/einvoice -export PATH="$PWD/bin:$PATH" +python3 scripts/install_commands.py # einmalig; auf diesem Rechner bereits eingerichtet einvoice --create \ --Customer "Meine Kundenfirma GmbH" \ @@ -103,21 +103,50 @@ Die Konsole zeigt Schritte, Versionsänderungen und Fehler; vollständige Logs l in `.runtime/logs/`. Netzwerk ist nur für neue Kandidaten nötig. Ohne `--apply` werden Downloads, Installation und Tests ausgeführt, aber keine Umschaltung. -**Für die verwaltete Laufzeit die Starter aus `bin/` verwenden:** +**Konsolenbefehle einmalig im Benutzer-Suchpfad einrichten:** ```bash -export PATH="/home/johannes/localdev/einvoice/bin:$PATH" # bei Bedarf in ~/.zshrc aufnehmen -# alternativ ohne PATH-Einrichtung: -./bin/einvoice --customers -./bin/einvoice --update --apply +python3 scripts/install_commands.py +# Danach aus jedem Verzeichnis, ohne venv-Aktivierung: +einvoice --customers +einvoice --update --apply +einvoice --update --rollback +jr-einvoice --help ``` -Diese PATH-Zeile nach einer eventuellen venv-Aktivierung ausführen. Direkte Aufrufe -von `.venv/bin/einvoice` verwenden weiterhin die ursprüngliche Entwicklerumgebung. +Der Installer verlinkt beide Starter nach `~/.local/bin` und prüft ihre Funktion. +Dort vorhandene andere Befehle werden vor dem Ersetzen gesichert. Wiederholtes +Ausführen ist möglich. Auf diesem Rechner ist das Verzeichnis bereits im PATH. +Bei einem gespeicherten alten Shell-Pfad einmal `rehash` (zsh) bzw. `hash -r` +(bash) ausführen. Eine aktivierte `.venv` kann die Befehle überdecken: dann +`deactivate` ausführen. Direkte Aufrufe von `.venv/bin/einvoice` verwenden weiterhin +die ursprüngliche Entwicklerumgebung. Java und native Bibliotheken werden mitgetestet, aber über den Systempaketmanager aktualisiert; dessen Änderungen kann dieser Updater nicht zurückrollen. Details und Grenzen: [Update-Betrieb](Documentation.md#updates-und-rollback). +## Ohne Microsoft Office / Microsoft 365 + +Die Rechnungslösung benötigt weder Word, Excel, Outlook noch Microsoft 365. +Es gibt keine DOC/DOCX-, XLS/XLSX- oder PPT/PPTX-Vorlagen und keine Office-Automation. +Auch LibreOffice muss zur Rechnungserstellung nicht installiert sein. + +| Zweck | Verwendetes Format | +| --- | --- | +| Rechnungsdaten und Stammdaten | YAML (`.yaml`) | +| Layout | HTML/Jinja2 (`.html.j2`), Logo als SVG | +| Fertige E-Rechnung | PDF/A-3 mit eingebettetem EN16931-XML | +| Optionaler Mailentwurf | EML (`.eml`), ohne Outlook-Anbindung | +| Dokumentation | Markdown (`.md`) | + +Ein Wechsel auf LibreOffice erfordert für dieses Projekt keine Dateikonvertierung. +ODT-/ODS-Dateien werden aktuell weder eingelesen noch erzeugt. Layoutänderungen +finden in der HTML-Vorlage statt. Das fertige E-Rechnungs-PDF direkt versenden; +nicht zum Bearbeiten konvertieren oder neu als PDF exportieren, da eingebettete +Rechnungsdaten oder die geprüfte PDF/A-Struktur verloren gehen können. +Die tatsächliche E-Mail-Zustellung erfolgt weiterhin über dein separat eingerichtetes +Mailprogramm bzw. Konto; sie wird von diesem Tool nicht bereitgestellt. + ## Daten und Einrichtung - Absender: **johannes@jr-it-services.de**, lokale Vorlage `private/presets/seller.default.yaml`. diff --git a/bin/einvoice b/bin/einvoice index bf91ceb..9c87ab7 100755 --- a/bin/einvoice +++ b/bin/einvoice @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +launcher="$(readlink -f -- "${BASH_SOURCE[0]}")" +root="$(cd -- "$(dirname -- "$launcher")/.." && pwd)" if [[ "${1:-}" == --update ]]; then shift exec python3 "$root/scripts/update.py" "$@" diff --git a/bin/jr-einvoice b/bin/jr-einvoice index bf91ceb..9c87ab7 100755 --- a/bin/jr-einvoice +++ b/bin/jr-einvoice @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +launcher="$(readlink -f -- "${BASH_SOURCE[0]}")" +root="$(cd -- "$(dirname -- "$launcher")/.." && pwd)" if [[ "${1:-}" == --update ]]; then shift exec python3 "$root/scripts/update.py" "$@" diff --git a/scripts/install_commands.py b/scripts/install_commands.py new file mode 100755 index 0000000..6d24b64 --- /dev/null +++ b/scripts/install_commands.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Install per-user launchers without sudo or changing shell configuration.""" +import argparse +from datetime import datetime, timezone +import os +from pathlib import Path +import subprocess +import sys +import uuid + + +def install(bin_dir: Path) -> None: + root = Path(__file__).resolve().parent.parent + bin_dir = bin_dir.expanduser().absolute() + bin_dir.mkdir(parents=True, exist_ok=True) + changed = [] + try: + for name in ('einvoice', 'jr-einvoice'): + source = root / 'bin' / name + target = bin_dir / name + if target.is_symlink() and target.resolve() == source: + print(f'Bereits eingerichtet: {target}', flush=True) + continue + backup = None + if target.exists() or target.is_symlink(): + if target.is_dir(): + raise RuntimeError(f'Ziel ist ein Verzeichnis: {target}') + stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ') + backup = target.with_name(f'{name}.before-einvoice-{stamp}-{uuid.uuid4().hex[:8]}') + if target.is_symlink(): + backup.symlink_to(os.readlink(target)) + else: + import shutil + shutil.copy2(target, backup) + print(f'Bisheriger Befehl gesichert: {backup}', flush=True) + temporary = target.with_name(f'.{name}-{uuid.uuid4().hex}.next') + try: + temporary.symlink_to(source) + os.replace(temporary, target) + finally: + temporary.unlink(missing_ok=True) + changed.append((target, backup)) + print(f'Eingerichtet: {target} -> {source}', flush=True) + # A different cwd verifies that symlinks resolve to the checkout correctly. + for name in ('einvoice', 'jr-einvoice'): + subprocess.run([str(bin_dir / name), '--help'], cwd=bin_dir, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + print('OK: Beide Konsolenbefehle geprüft.', flush=True) + except BaseException: + for target, backup in reversed(changed): + if backup is None: + target.unlink(missing_ok=True) + else: + os.replace(backup, target) + raise + if str(bin_dir) not in os.environ.get('PATH', '').split(os.pathsep): + import shlex + print(f'Für diese Shell ausführen: export PATH={shlex.quote(str(bin_dir))}:"$PATH"') + print('Ab jetzt: einvoice --help | einvoice --update --status | jr-einvoice --help') + print('Falls die Shell noch den alten Pfad merkt: rehash (zsh) bzw. hash -r (bash).') + + +def main(): + parser = argparse.ArgumentParser(description='einvoice und jr-einvoice im Benutzer-Suchpfad einrichten.') + parser.add_argument('--bin-dir', type=Path, default=Path.home() / '.local/bin') + args = parser.parse_args() + try: + install(args.bin_dir) + except (Exception, KeyboardInterrupt) as exc: + print(f'FEHLER: {exc}; geänderte Befehle wurden zurückgesetzt.', file=sys.stderr) + if isinstance(exc, subprocess.CalledProcessError) and exc.stderr: + print(exc.stderr.strip(), file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main())