feat: create validated ZUGFeRD invoices in one console command
This commit is contained in:
1 parent
e0c15fc7f2
commit
a643cf5227
30 files changed
+1509
-207
No files matched your search
+61
-3
@@ -18,13 +18,17 @@ app = typer.Typer(add_completion=False, no_args_is_help=True)
|
||||
console = Console()
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
return root if (root / "templates/invoice.html.j2").is_file() else Path(__file__).resolve().parent / "data"
|
||||
|
||||
def _default_template() -> Path:
|
||||
return _repo_root() / "templates" / "invoice.html.j2"
|
||||
|
||||
def _mustang_default_path() -> Path:
|
||||
return _repo_root() / "tools" / "mustang" / "Mustang-CLI.jar"
|
||||
root = _repo_root()
|
||||
if root.name == "data":
|
||||
return Path.home() / ".local/share/jr-einvoice/tools/Mustang-CLI.jar"
|
||||
return root / "tools" / "mustang" / "Mustang-CLI.jar"
|
||||
|
||||
def _mustang_dir() -> Path:
|
||||
return _mustang_default_path().parent
|
||||
@@ -46,7 +50,7 @@ def _fail(msg: str, exc: Exception | None = None, exit_code: int = 1):
|
||||
|
||||
@app.command("setup-mustang")
|
||||
def setup_mustang(
|
||||
version: str = typer.Option("2.22.0", "--version", help="Mustang-CLI version to download"),
|
||||
version: str = typer.Option("2.26.0", "--version", help="Mustang-CLI version to download"),
|
||||
dest: Path = typer.Option(_mustang_default_path(), "--dest", help="Where to place Mustang-CLI.jar"),
|
||||
verify_sha1: bool = typer.Option(True, "--verify-sha1/--no-verify-sha1", help="Verify downloaded jar using Maven .sha1 file"),
|
||||
log_level: str = typer.Option("INFO", "--log-level", help="Console log level: DEBUG/INFO/WARNING/ERROR"),
|
||||
@@ -156,6 +160,7 @@ def generate(
|
||||
print_artifacts_only: bool = typer.Option(False, "--print-artifacts-only", help="Only print artifact paths (no rich output)"),
|
||||
json_out: bool = typer.Option(False, "--json", help="Print artifacts as JSON to stdout (best for CI)"),
|
||||
):
|
||||
invoice_dir = invoice_dir.resolve()
|
||||
try:
|
||||
_ = load_invoice_yaml(invoice_dir / invoice_filename)
|
||||
except Exception as e:
|
||||
@@ -228,3 +233,56 @@ def generate(
|
||||
except Exception as e:
|
||||
logger.exception("Generate failed")
|
||||
_fail("Generate failed. See out/logs/run.log for details.", e)
|
||||
|
||||
@app.command("monthly")
|
||||
def monthly(
|
||||
root: Path = typer.Argument(..., help="Private invoice root"),
|
||||
customer: str = typer.Option(..., "--customer", "-c"),
|
||||
month: str = typer.Option(..., "--month", help="Service month YYYY-MM"),
|
||||
hours: str = typer.Option(..., "--hours", help="Hours, decimal point allowed"),
|
||||
number: str = typer.Option(..., "--number", "-n", help="Unique invoice number, including existing PDF invoices"),
|
||||
rate: str | None = typer.Option(None, "--rate", help="Override preset hourly net rate"),
|
||||
issued: str | None = typer.Option(None, "--date", help="Invoice date YYYY-MM-DD; default today"),
|
||||
due_days: int = typer.Option(30, "--due-days", min=0),
|
||||
):
|
||||
"""Monatsrechnung mit Stunden, Zeitraum, Kundenstammdaten und Zahlungsziel anlegen."""
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
from .monthly import monthly_invoice
|
||||
try:
|
||||
target = monthly_invoice(root, _repo_root(), setup_logging("INFO", None), customer,
|
||||
month, Decimal(hours), number, date.fromisoformat(issued) if issued else None,
|
||||
Decimal(rate) if rate else None, due_days)
|
||||
sys.stdout.write(str(target) + "\n")
|
||||
except Exception as e:
|
||||
_fail("Monatsrechnung konnte nicht angelegt werden", e)
|
||||
|
||||
@app.command("email")
|
||||
def email_draft(
|
||||
invoice_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
||||
recipient: str | None = typer.Option(None, "--to", help="Agreed invoice recipient"),
|
||||
mustang_jar: Path | None = typer.Option(None, "--mustang-jar"),
|
||||
):
|
||||
"""Rechnung neu erzeugen, validieren und Versandentwurf (.eml) mit PDF-Anhang vorbereiten."""
|
||||
from .delivery import prepare_email
|
||||
logger = setup_logging("INFO", invoice_dir / 'out/logs/run.log')
|
||||
try:
|
||||
path = prepare_email(invoice_dir.resolve(), mustang_jar or _default_mustang_jar(), logger, recipient)
|
||||
console.print(f"Versandentwurf: {path}\nIm Mailprogramm öffnen, prüfen und absenden.")
|
||||
except Exception as e:
|
||||
logger.exception("Email preparation failed")
|
||||
_fail("Versandentwurf konnte nicht erstellt werden", e)
|
||||
|
||||
@app.command("validate")
|
||||
def validate_file(
|
||||
source: Path = typer.Argument(..., exists=True, dir_okay=False),
|
||||
report: Path = typer.Option(Path("validation-report.xml"), "--report"),
|
||||
mustang_jar: Path | None = typer.Option(None, "--mustang-jar"),
|
||||
):
|
||||
"""Eingehende XML- oder ZUGFeRD-PDF-Rechnung lokal technisch prüfen."""
|
||||
from .mustang import validate
|
||||
try:
|
||||
validate(mustang_jar or _default_mustang_jar(), source.resolve(), report, setup_logging("INFO", None))
|
||||
console.print(f"Technisch gültig. Prüfbericht: {report}")
|
||||
except Exception as e:
|
||||
_fail("Validierung fehlgeschlagen", e)
|
||||
Reference in new issue
Block a user