From b882f783fc809d835b15da3939bc08870eda6055 Mon Sep 17 00:00:00 2001
From: Philipp Wolak
Date: Mon, 4 May 2026 12:42:26 +0200
Subject: [PATCH] Use xrechnung.xml for XRECHNUNG attachments
---
drafthorse/pdf.py | 10 ++++++++--
tests/test_roundtrip.py | 10 ++++++++++
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/drafthorse/pdf.py b/drafthorse/pdf.py
index a5bc124..a157589 100644
--- a/drafthorse/pdf.py
+++ b/drafthorse/pdf.py
@@ -149,6 +149,10 @@ def _prepare_pdf_metadata_txt(pdf_metadata):
}
+def _get_xml_attachment_filename(profile):
+ return "xrechnung.xml" if profile == "XRECHNUNG" else "factur-x.xml"
+
+
def _prepare_xmp_metadata(profile, pdf_metadata):
"""
Prepare pdf metadata using the FACTUR-X XMP extension schema
@@ -161,6 +165,7 @@ def _prepare_xmp_metadata(profile, pdf_metadata):
escaped_metadata = {
key: xml_escape(str(value)) for key, value in pdf_metadata.items()
}
+ xml_filename = _get_xml_attachment_filename(profile)
xml_str = XMP_SCHEMA.format(
title=escaped_metadata.get("title", ""),
@@ -171,7 +176,7 @@ def _prepare_xmp_metadata(profile, pdf_metadata):
timestamp=datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00"),
urn="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#",
documenttype="INVOICE",
- xml_filename="factur-x.xml",
+ xml_filename=xml_filename,
version="1.0",
xmp_level=profile,
)
@@ -222,7 +227,8 @@ def _update_metadata_add_attachment(
{NameObject("/F"): file_entry_obj, NameObject("/UF"): file_entry_obj}
)
- fname_obj = create_string_object("factur-x.xml")
+ xml_filename = _get_xml_attachment_filename(facturx_level)
+ fname_obj = create_string_object(xml_filename)
filespec_dict = DictionaryObject(
{
NameObject("/AFRelationship"): NameObject(
diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py
index d745cca..bb3a1d7 100644
--- a/tests/test_roundtrip.py
+++ b/tests/test_roundtrip.py
@@ -58,6 +58,16 @@ def test_sample_roundtrip(filename):
# Read back the PDF. We don't support extensive parsing, but this way we can assert that metadata is at least present
# and syntactically valid.
pdf_reader = PdfReader(BytesIO(created_pdf_bytes))
+ expected_xml_filename = (
+ "xrechnung.xml" if filename.split("_")[2] == "XRECHNUNG" else "factur-x.xml"
+ )
+ embedded_names = (
+ pdf_reader.trailer["/Root"]["/Names"]["/EmbeddedFiles"]["/Names"]
+ ).get_object()
+ metadata_xml = pdf_reader.trailer["/Root"]["/Metadata"].get_object().get_data()
+
+ assert str(embedded_names[0]) == expected_xml_filename
+ assert expected_xml_filename.encode() in metadata_xml
assert pdf_reader.xmp_metadata
# Parse the sample file into our internal python structure