feat: install user console commands and document Office independence

This commit is contained in:
admin committed 2026-10-16 12:30:46 +02:00
1 parent 6438558ea3
commit f447603af4
6 files changed
+179 -10

No files matched your search

+56
View File
@@ -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-<Zeit-ID>`
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.
+4
View File
@@ -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
+37 -8
View File
@@ -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`.
+2 -1
View File
@@ -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" "$@"
+2 -1
View File
@@ -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" "$@"
+78
View File
@@ -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())