feat: create validated ZUGFeRD invoices in one console command

This commit is contained in:
admin committed 2026-10-16 09:29:08 +02:00
1 parent e0c15fc7f2
commit a643cf5227
30 files changed
+1509 -207

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
"""Prepare a reviewed email draft; never sends mail automatically."""
from email.message import EmailMessage
from email.policy import SMTP
from pathlib import Path
import hashlib
import json
from .io import load_invoice_yaml
def prepare_email(invoice_dir: Path, jar: Path, logger, recipient: str | None = None) -> Path:
# Invalidate any previous draft before rebuilding.
out = invoice_dir / 'out'
(out / 'invoice.eml').unlink(missing_ok=True)
(out / 'manifest.json').unlink(missing_ok=True)
inv = load_invoice_yaml(invoice_dir / 'invoice.yaml')
recipient = recipient or inv.buyer.email
import re
if not recipient or not re.fullmatch(r'[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+', recipient):
raise ValueError('Set buyer.email or pass --to with the agreed invoice address')
out = invoice_dir / 'out'
# Rebuild from current YAML so the draft cannot attach an obsolete invoice.
from .pipeline import build_in_folder
template = Path(__file__).resolve().parent.parent / 'templates/invoice.html.j2'
if not template.is_file():
template = Path(__file__).resolve().parent / 'data/templates/invoice.html.j2'
result = build_in_folder(invoice_dir, 'out', template, logger, jar, True, True)
pdf = result.facturx_pdf
msg = EmailMessage(policy=SMTP)
msg['From'] = inv.seller.email or ''
if not inv.seller.email:
raise ValueError('seller.email is required for an email draft')
msg['To'] = recipient
msg['Subject'] = f'Rechnung {inv.meta.invoice_number} – {inv.seller.name}'
msg['X-Unsent'] = '1'
msg.set_content(f'Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie unsere E-Rechnung {inv.meta.invoice_number} als ZUGFeRD/Factur-X-PDF mit eingebetteten strukturierten Rechnungsdaten.\n\nVielen Dank für Ihren Auftrag.\n\nFreundliche Grüße\n{inv.seller.name}\n')
msg.add_attachment(pdf.read_bytes(), maintype='application', subtype='pdf', filename=f'Rechnung_{_safe_name(inv.meta.invoice_number)}.pdf')
target = out / 'invoice.eml'
target.write_bytes(msg.as_bytes())
manifest = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in
[invoice_dir / 'invoice.yaml', result.facturx_xml, pdf, result.validate_report, target]}
(out / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')
return target
def _safe_name(number: str) -> str:
import re
return re.sub(r'[^A-Za-z0-9_-]', '_', number)