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
@@ -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)
|
||||
Reference in new issue
Block a user