Repository: pretix/python-drafthorse

Issue:
Parser should not fail on unknown elements
There are and likely always will be some legal elements that we forgot to define in our python structure. The parser should not fail when it encounters such unknown elements.

For example, the formerly unknown element that is introduced in https://github.com/pretix/python-drafthorse/pull/50 should not have made the parser fail. The element is rather unimportant. I'd still like to read the rest of the invoice, even when this piece of information gets lost.

### Reproduce

Terminal:

```bash
mkdir drafthorse-51
cd drafthorse-51
python3 -m venv env
source env/bin/activate
pip install drafthorse
curl https://raw.githubusercontent.com/ConnectingEurope/eInvoicing-EN16931/7ce3772aff315588f37e38b509173f253d340e45/cii/examples/CII_example5.xml > sample.xml
python
```

REPL:
```python
>>> from drafthorse.models.document import Document
>>> samplexml = open("sample.xml", "rb").read()
... 
>>> doc = Document.parse(samplexml)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    doc = Document.parse(samplexml)
  File "/Users/raffael/Code/drafthorse-51/env/lib/python3.13/site-packages/drafthorse/models/elements.py", line 124, in parse
    return cls().from_etree(root)
           ~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/raffael/Code/drafthorse-51/env/lib/python3.13/site-packages/drafthorse/models/elements.py", line 114, in from_etree
    getattr(self, name).from_etree(child)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/raffael/Code/drafthorse-51/env/lib/python3.13/site-packages/drafthorse/models/elements.py", line 114, in from_etree
    getattr(self, name).from_etree(child)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/raffael/Code/drafthorse-51/env/lib/python3.13/site-packages/drafthorse/models/elements.py", line 116, in from_etree
    raise TypeError("Unknown element {}".format(child.tag))
TypeError: Unknown element {urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100}ReceivingAdviceReferencedDocument
```

Hints from issue comments:
@raphaelm: Hmm, I'm undecided on this one. Currently, the library is "roundtrip-safe", i.e. if you open an XML and serialize it back out, you can be sure to have all information in there. With fixing this like #52 proposes, this would be lost, which could lead to unexpected bugs if someone relies on this property for some post-processing of documents. But I agree that there are legitimate cases for unknown elements to be in there. So we need to decide how important this roundtip-mode is to us and maybe either introduce a "strict mode" where the validation is turned back on, or store the unexpected elements in a way we can serialize them again (likely too much effort).@barredterra: "strict mode" sounds fine to me. This could even be enabled by default, so we don't have any breaking changes.

Solution patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5a5c3bc..2a3d452 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,9 +13,8 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - "3.8"
-          - "3.9"
-          - "3.10"
+          - "3.11"
+          - "3.12"
     steps:
       - uses: actions/checkout@v2
       - name: Install Dependencies
@@ -25,7 +24,7 @@ jobs:
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install Dependencies
-        run: python -m pip install -U pip wheel coverage codecov pytest
+        run: python -m pip install -U pip wheel setuptools -r requirements_dev.txt
       - name: Install Dependencies
         run: python setup.py develop
       - name: Run tests
@@ -44,7 +43,7 @@ jobs:
         with:
           python-version: "3.10"
       - name: Install Dependencies
-        run: python -m pip install -U pip wheel black isort pytest
+        run: python -m pip install -U pip wheel setuptools -r requirements_dev.txt
       - name: Install Dependencies
         run: python setup.py develop
       - name: Run black
diff --git a/README.rst b/README.rst
index 6018efd..07cac92 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,8 @@
 drafthorse -- Basic ZUGFeRD implementation in Python
 ====================================================
 
-.. image:: https://travis-ci.com/pretix/python-drafthorse.svg?branch=master
-   :target: https://travis-ci.com/pretix/python-drafthorse
+.. image:: https://github.com/pretix/python-drafthorse/actions/workflows/tests.yml/badge.svg
+   :target: https://github.com/pretix/python-drafthorse/actions/workflows/tests.yml
 
 .. image:: https://codecov.io/gh/pretix/python-drafthorse/branch/master/graph/badge.svg
    :target: https://codecov.io/gh/pretix/drafthorse
@@ -17,7 +17,7 @@ attach it to a PDF. We do not support parsing PDF files (for now).
 
 By low-level, we mean that this library tries to model the ZUGFeRD data model 1:1 without any
 further abstractions or simplifications. You can set and parse all parameters defined in ZUGFeRD
-2.2. To see which parameters are defined, please refer to the official ZUGFeRD specification,
+2.3. To see which parameters are defined, please refer to the official ZUGFeRD specification,
 there is no documentation for this library that reiterates it.
 
 All output is validated against the official XSDs, but no validation of profile levels
@@ -45,6 +45,8 @@ Parsing::
     doc = Document.parse(samplexml)
     print(doc.trade.agreement.seller.name)
 
+``Document.parse()`` taskes a boolean parameter ``strict`` which defaults to ``True``. This means that the parser will raise an error if it encounters any unknown element. If you set it to ``False``, the parser will not raise an error and parse whatever it can.
+
 Generating::
 
     import os
@@ -54,6 +56,7 @@ Generating::
     from drafthorse.models.accounting import ApplicableTradeTax
     from drafthorse.models.document import Document
     from drafthorse.models.note import IncludedNote
+    from drafthorse.models.party import TaxRegistration
     from drafthorse.models.tradelines import LineItem
     from drafthorse.pdf import attach_xml
 
@@ -71,7 +74,7 @@ Generating::
     doc.header.notes.add(note)
 
     doc.trade.agreement.seller.name = "Lieferant GmbH"
-    doc.trade.settlement.payee.name = "Kunde GmbH"
+    doc.trade.settlement.payee.name = "Lieferant GmbH"
 
     doc.trade.agreement.buyer.name = "Kunde GmbH"
     doc.trade.settlement.invoicee.name = "Kunde GmbH"
@@ -81,6 +84,11 @@ Generating::
 
     doc.trade.agreement.seller.address.country_id = "DE"
     doc.trade.agreement.seller.address.country_subdivision = "Bayern"
+    doc.trade.agreement.seller.tax_registrations.add(
+        TaxRegistration(
+            id=("VA", "DE000000000")
+        )
+    )
 
     doc.trade.agreement.seller_order.issue_date_time = datetime.now(timezone.utc)
     doc.trade.agreement.buyer_order.issue_date_time = datetime.now(timezone.utc)
@@ -98,7 +106,7 @@ Generating::
     li.settlement.trade_tax.type_code = "VAT"
     li.settlement.trade_tax.category_code = "E"
     li.settlement.trade_tax.rate_applicable_percent = Decimal("0.00")
-    li.settlement.monetary_summation.total_amount = Decimal("999.00")
+    li.settlement.monetary_summation.total_amount = (Decimal("999.00"), "EUR")
     doc.trade.items.add(li)
 
     trade_tax = ApplicableTradeTax()
@@ -114,7 +122,7 @@ Generating::
     doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00")
     doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00")
     doc.trade.settlement.monetary_summation.tax_basis_total = Decimal("999.00")
-    doc.trade.settlement.monetary_summation.tax_total = Decimal("0.00")
+    doc.trade.settlement.monetary_summation.tax_total = (Decimal("0.00"), "EUR")
     doc.trade.settlement.monetary_summation.grand_total = Decimal("999.00")
     doc.trade.settlement.monetary_summation.due_amount = Decimal("999.00")
 
@@ -130,6 +138,7 @@ Generating::
     with open("output.pdf", "wb") as f:
         f.write(new_pdf_bytes)
 
+``Document.serialize()`` will validate the generated XML against the specified schema and raise an error if it is not valid. If you want to avoid validation, you can set the ``schema`` parameter to ``None``.
 
 Development
 -----------
diff --git a/drafthorse/models/__init__.py b/drafthorse/models/__init__.py
index b4a25e2..f82d0aa 100644
--- a/drafthorse/models/__init__.py
+++ b/drafthorse/models/__init__.py
@@ -1,6 +1,5 @@
 NS_RSM = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
 NS_UDT = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
-NS_A = "urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
 NS_RAM = (
     "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
 )
diff --git a/drafthorse/models/container.py b/drafthorse/models/container.py
index eb1755e..f5ec87e 100644
--- a/drafthorse/models/container.py
+++ b/drafthorse/models/container.py
@@ -19,9 +19,9 @@ class Container:
     def empty_element(self):
         return self.child_type()
 
-    def add_from_etree(self, root):
+    def add_from_etree(self, root, strict=True):
         childel = self.empty_element()
-        childel.from_etree(root)
+        childel.from_etree(root, strict)
         self.add(childel)
 
 
@@ -46,7 +46,7 @@ class SimpleContainer(Container):
             self.set_element(el, child)
             el.append_to(node)
 
-    def add_from_etree(self, root):
+    def add_from_etree(self, root, strict=True):
         self.add(root.text)
 
 
@@ -60,7 +60,7 @@ class CurrencyContainer(SimpleContainer):
         el._amount = child[0]
         el._currency = child[1]
 
-    def add_from_etree(self, root):
+    def add_from_etree(self, root, strict=True):
         self.add((root.text, root.attrib.get("currencyID")))
 
 
@@ -74,7 +74,7 @@ class IDContainer(SimpleContainer):
         el._text = child[1]
         el._scheme_id = child[0]
 
-    def add_from_etree(self, root):
+    def add_from_etree(self, root, strict=True):
         self.add((root.attrib["schemeID"], root.text))
 
 
@@ -87,5 +87,5 @@ class StringContainer(SimpleContainer):
     def set_element(self, el, child):
         el._text = child
 
-    def add_from_etree(self, root):
+    def add_from_etree(self, root, strict=True):
         self.add(root.text)
diff --git a/drafthorse/models/delivery.py b/drafthorse/models/delivery.py
index c51ab96..ca8ef00 100644
--- a/drafthorse/models/delivery.py
+++ b/drafthorse/models/delivery.py
@@ -1,4 +1,4 @@
-from . import BASIC, EXTENDED, NS_RAM
+from . import BASIC, COMFORT, EXTENDED, NS_RAM
 from .elements import Element
 from .fields import DateTimeField, Field, StringField
 from .party import (
@@ -9,6 +9,7 @@ from .party import (
 from .references import (
     DeliveryNoteReferencedDocument,
     DespatchAdviceReferencedDocument,
+    ReceivingAdviceReferencedDocument,
 )
 
 
@@ -49,11 +50,10 @@ class TradeDelivery(Element):
         SupplyChainConsignment,
         default=False,
         required=False,
+        profile=EXTENDED,
         _d="Detailinformationen zur Konsignation oder Sendung",
     )
-    ship_to: ShipToTradeParty = Field(
-        ShipToTradeParty, required=False, profile=EXTENDED
-    )
+    ship_to: ShipToTradeParty = Field(ShipToTradeParty, required=False, profile=COMFORT)
     ultimate_ship_to: UltimateShipToTradeParty = Field(
         UltimateShipToTradeParty, required=False, profile=EXTENDED
     )
@@ -62,11 +62,14 @@ class TradeDelivery(Element):
     )
     event: SupplyChainEvent = Field(SupplyChainEvent, required=False, profile=BASIC)
     despatch_advice: DespatchAdviceReferencedDocument = Field(
-        DespatchAdviceReferencedDocument, required=False, profile=EXTENDED
+        DespatchAdviceReferencedDocument, required=False, profile=COMFORT
     )
     delivery_note: DeliveryNoteReferencedDocument = Field(
         DeliveryNoteReferencedDocument, required=False, profile=EXTENDED
     )
+    receiving_advice: ReceivingAdviceReferencedDocument = Field(
+        ReceivingAdviceReferencedDocument, required=False, profile=COMFORT
+    )
 
     class Meta:
         namespace = NS_RAM
diff --git a/drafthorse/models/document.py b/drafthorse/models/document.py
index 67a7920..bbdcc11 100644
--- a/drafthorse/models/document.py
+++ b/drafthorse/models/document.py
@@ -3,7 +3,7 @@ import xml.etree.cElementTree as ET
 from .container import StringContainer, Container
 from .note import IncludedNote
 
-from . import BASIC, EXTENDED, NS_A, NS_QDT, NS_RAM, NS_RSM, NS_UDT
+from . import BASIC, EXTENDED, NS_QDT, NS_RAM, NS_RSM, NS_UDT
 from .elements import Element
 from .fields import (
     DateTimeField,
@@ -100,15 +100,30 @@ class Document(Element):
     context: DocumentContext = Field(DocumentContext, required=True)
     header: Header = Field(Header, required=True)
     trade: TradeTransaction = Field(TradeTransaction, required=True)
+    __namespaces = {
+        "rsm": NS_RSM,
+        "qdt": NS_QDT,
+        "ram": NS_RAM,
+        "xs": "http://www.w3.org/2001/XMLSchema",
+        "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+        "udt": NS_UDT,
+    }
 
     def __init__(self):
         super().__init__()
-        ET.register_namespace("a", NS_A)
-        ET.register_namespace("rsm", NS_RSM)
-        ET.register_namespace("qdt", NS_QDT)
-        ET.register_namespace("ram", NS_RAM)
-        ET.register_namespace("xsi", "http://www.w3.org/2001/XMLSchema")
-        ET.register_namespace("udt", NS_UDT)
+        for ns, url in self.__namespaces.items():
+            ET.register_namespace(ns, url)
+
+    def serialize(self, schema="FACTUR-X_BASIC"):
+        # First pass
+        xml = super().serialize(schema)
+
+        # Second pass to ensure all namespaces are defined even if they are unused
+        root = ET.fromstring(xml)
+        for ns, url in self.__namespaces.items():
+            if ns.encode() not in xml:
+                root.set(f"xmlns:{ns}", url)
+        return ET.tostring(root, "utf-8")
 
     class Meta:
         namespace = NS_RSM
diff --git a/drafthorse/models/elements.py b/drafthorse/models/elements.py
index ff27264..2d763b7 100644
--- a/drafthorse/models/elements.py
+++ b/drafthorse/models/elements.py
@@ -88,7 +88,7 @@ class Element(metaclass=BaseElementMeta):
             )
         return super().__setattr__(key, value)
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         if (
             hasattr(self, "Meta")
             and hasattr(self.Meta, "namespace")
@@ -109,19 +109,19 @@ class Element(metaclass=BaseElementMeta):
             if child.tag in field_index:
                 name, _childel = field_index[child.tag]
                 if isinstance(getattr(self, name), Container):
-                    getattr(self, name).add_from_etree(child)
+                    getattr(self, name).add_from_etree(child, strict)
                 else:
-                    getattr(self, name).from_etree(child)
-            else:
+                    getattr(self, name).from_etree(child, strict)
+            elif strict:
                 raise TypeError("Unknown element {}".format(child.tag))
         return self
 
     @classmethod
-    def parse(cls, xmlinput):
+    def parse(cls, xmlinput, strict=True):
         from lxml import etree
 
         root = etree.fromstring(xmlinput)
-        return cls().from_etree(root)
+        return cls().from_etree(root, strict)
 
 
 class StringElement(Element):
@@ -149,7 +149,7 @@ class StringElement(Element):
         node.text = self._text
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._text = root.text
         self._set_on_input = True
         return self
@@ -168,7 +168,7 @@ class DecimalElement(StringElement):
     def __str__(self):
         return self._value
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._value = Decimal(root.text)
         self._set_on_input = True
         return self
@@ -189,7 +189,7 @@ class QuantityElement(StringElement):
     def __str__(self):
         return "{} {}".format(self._amount, self._unit_code)
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._amount = Decimal(root.text)
         self._unit_code = root.attrib["unitCode"]
         self._set_on_input = True
@@ -211,7 +211,7 @@ class CurrencyElement(StringElement):
             del node.attrib["currencyID"]
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._amount = Decimal(root.text)
         self._currency = root.attrib.get("currencyID") or None
         self._set_on_input = True
@@ -235,7 +235,7 @@ class ClassificationElement(StringElement):
         node.attrib["listVersionID"] = self._list_version_id
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._text = Decimal(root.text)
         self._list_id = root.attrib["listID"]
         self._list_version_id = root.attrib["listVersionID"]
@@ -260,7 +260,7 @@ class BinaryObjectElement(StringElement):
         node.text = self._text
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._mime_code = root.attrib["mimeCode"]
         self._filename = root.attrib["filename"]
         self._text = root.text
@@ -283,7 +283,7 @@ class AgencyIDElement(StringElement):
         node.attrib["schemeAgencyID"] = self._scheme_id
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._text = root.text
         self._scheme_id = root.attrib["schemeAgencyID"]
         self._set_on_input = True
@@ -306,7 +306,7 @@ class IDElement(StringElement):
             node.attrib["schemeID"] = self._scheme_id
         return node
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         self._text = root.text
         try:
             self._scheme_id = root.attrib["schemeID"]
@@ -346,11 +346,14 @@ class DateTimeElement(StringElement):
             t.append(node)
         return t
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         if len(root) != 1:
             raise TypeError("Date containers should have one child")
         if root[0].tag != "{%s}%s" % (self._date_time_namespace, "DateTimeString"):
-            raise TypeError("Tag %s not recognized" % root[0].tag)
+            if strict:
+                raise TypeError("Tag %s not recognized" % root[0].tag)
+            else:
+                return self
         self._format = root[0].attrib["format"]
         if self._format == "102":
             self._value = datetime.strptime(root[0].text, "%Y%m%d").date()
@@ -362,7 +365,7 @@ class DateTimeElement(StringElement):
                 self._value = w.monday()
             else:
                 self._value = datetime.strptime(root[0].text + "1", "%G%V%u").date()
-        else:
+        elif strict:
             raise TypeError(
                 "Date format %s cannot be parsed" % root[0].attrib["format"]
             )
@@ -384,7 +387,7 @@ class DirectDateTimeElement(StringElement):
             t.text = self._value.strftime("%Y-%m-%dT%H:%M:%S")
         return t
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         try:
             self._value = datetime.strptime(root.text, "%Y-%m-%dT%H:%M:%S").date()
         except Exception:
@@ -416,7 +419,7 @@ class IndicatorElement(StringElement):
     def __str__(self):
         return "{}".format(self._value)
 
-    def from_etree(self, root):
+    def from_etree(self, root, strict=True):
         if len(root) != 1:
             raise TypeError("Indicator containers should have one child")
         if root[0].tag != "{%s}%s" % (NS_UDT, "Indicator"):
diff --git a/drafthorse/models/party.py b/drafthorse/models/party.py
index 3d0dd5d..fa8d00e 100644
--- a/drafthorse/models/party.py
+++ b/drafthorse/models/party.py
@@ -130,7 +130,7 @@ class TradeParty(Element):
         profile=BASIC,
         _d="Anschrift des Handelspartners",
     )
-    electronic_address: Container = MultiField(
+    electronic_address: Container = Field(
         URIUniversalCommunication, required=False, profile=BASIC
     )
     tax_registrations: Container = MultiField(
diff --git a/drafthorse/models/product.py b/drafthorse/models/product.py
index 136c3aa..fa34369 100644
--- a/drafthorse/models/product.py
+++ b/drafthorse/models/product.py
@@ -97,14 +97,10 @@ class TradeProduct(Element):
     industry_assigned_id = IDField(
         NS_RAM, "IndustryAssignedID", required=False, profile=EXTENDED
     )
-    model_id = IDField(
-        NS_RAM, "ModelID", required=False, profile=EXTENDED
-    )
+    model_id = IDField(NS_RAM, "ModelID", required=False, profile=EXTENDED)
     name = StringField(NS_RAM, "Name", required=False)
     description = StringField(NS_RAM, "Description", required=False, profile=COMFORT)
-    batch_id = IDField(
-        NS_RAM, "BatchID", required=False, profile=EXTENDED
-    )
+    batch_id = IDField(NS_RAM, "BatchID", required=False, profile=EXTENDED)
     brand_name = StringField(NS_RAM, "BrandName", required=False, profile=EXTENDED)
     model_name = StringField(NS_RAM, "ModelName", required=False, profile=EXTENDED)
     characteristics: Container = MultiField(
diff --git a/drafthorse/models/references.py b/drafthorse/models/references.py
index d1cda2d..bed7df2 100644
--- a/drafthorse/models/references.py
+++ b/drafthorse/models/references.py
@@ -116,6 +116,12 @@ class LineDespatchAdviceReferencedDocument(ReferencedDocument):
         tag = "DespatchAdviceReferencedDocument"
 
 
+class ReceivingAdviceReferencedDocument(ReferencedDocument):
+    class Meta:
+        namespace = NS_RAM
+        tag = "ReceivingAdviceReferencedDocument"
+
+
 class LineReceivingAdviceReferencedDocument(ReferencedDocument):
     line_id = StringField(NS_RAM, "LineID", required=False, profile=COMFORT)
 
diff --git a/drafthorse/models/trade.py b/drafthorse/models/trade.py
index e6d2822..75dc566 100644
--- a/drafthorse/models/trade.py
+++ b/drafthorse/models/trade.py
@@ -77,9 +77,6 @@ class TradeAgreement(Element):
     buyer_order: BuyerOrderReferencedDocument = Field(
         BuyerOrderReferencedDocument, required=False
     )
-    customer_order: UltimateCustomerOrderReferencedDocument = Field(
-        UltimateCustomerOrderReferencedDocument, required=False, profile=COMFORT
-    )
     contract: ContractReferencedDocument = Field(
         ContractReferencedDocument, required=False, profile=COMFORT
     )
@@ -93,6 +90,9 @@ class TradeAgreement(Element):
     procuring_project_type: ProcuringProjectType = Field(
         ProcuringProjectType, required=False
     )
+    customer_order: UltimateCustomerOrderReferencedDocument = Field(
+        UltimateCustomerOrderReferencedDocument, required=False, profile=COMFORT
+    )
 
     class Meta:
         namespace = NS_RAM
@@ -164,7 +164,9 @@ class AdvancePayment(Element):
         NS_RAM, "FormattedReceivedDateTime", date_time_namespace=NS_QDT
     )
     included_trade_tax: Container = MultiField(IncludedTradeTax)
-    referenced_document: InvoiceSpecifiedReferencedDocument = Field(InvoiceSpecifiedReferencedDocument, required=False, profile=EXTENDED)
+    referenced_document: InvoiceSpecifiedReferencedDocument = Field(
+        InvoiceSpecifiedReferencedDocument, required=False, profile=EXTENDED
+    )
 
     class Meta:
         namespace = NS_RAM
diff --git a/drafthorse/models/tradelines.py b/drafthorse/models/tradelines.py
index 095ea0c..dd2a921 100644
--- a/drafthorse/models/tradelines.py
+++ b/drafthorse/models/tradelines.py
@@ -168,8 +168,10 @@ class LineSettlement(Element):
         LineSummation, required=False, profile=BASIC
     )
     invoice_referenced_document: Container = MultiField(
-        InvoiceReferencedDocument, required=False, profile=EXTENDED,
-        _d="Referenz auf die vorausgegangene Rechnung"
+        InvoiceReferencedDocument,
+        required=False,
+        profile=EXTENDED,
+        _d="Referenz auf die vorausgegangene Rechnung",
     )
     additional_referenced_document: LineAdditionalReferencedDocument = Field(
         LineAdditionalReferencedDocument, required=False, profile=COMFORT
diff --git a/drafthorse/pdf.py b/drafthorse/pdf.py
index 3b07377..a17709a 100644
--- a/drafthorse/pdf.py
+++ b/drafthorse/pdf.py
@@ -124,8 +124,10 @@ def _get_original_output_intents(original_pdf):
                 "/DestOutputProfile"
             ].get_object()
             output_intents.append((ori_output_intent_dict, dest_output_profile_dict))
+    except KeyError as ex:
+        logger.debug("Data missing from PDF: %s", ex)
     except Exception as ex:
-        logger.error(ex)
+        logger.exception(ex)
     return output_intents
 
 
diff --git a/requirements_dev.txt b/requirements_dev.txt
index ec0c289..17ce34c 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -3,3 +3,6 @@ pypdf
 pytest
 flake8
 isort
+black
+coverage
+codecov


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 04abdb4..b3d092d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,6 +6,7 @@ from decimal import Decimal
 from drafthorse.models.accounting import ApplicableTradeTax
 from drafthorse.models.document import Document
 from drafthorse.models.note import IncludedNote
+from drafthorse.models.party import TaxRegistration
 from drafthorse.models.tradelines import LineItem
 
 
@@ -26,7 +27,7 @@ def invoice_document(request):
     doc.header.notes.add(note)
 
     doc.trade.agreement.seller.name = "Lieferant GmbH"
-    doc.trade.settlement.payee.name = "Kunde GmbH"
+    doc.trade.settlement.payee.name = "Lieferant GmbH"
 
     doc.trade.agreement.buyer.name = "Kunde GmbH"
     doc.trade.settlement.invoicee.name = "Kunde GmbH"
@@ -36,6 +37,9 @@ def invoice_document(request):
 
     doc.trade.agreement.seller.address.country_id = "DE"
     doc.trade.agreement.seller.address.country_subdivision = "Bayern"
+    doc.trade.agreement.seller.tax_registrations.add(
+        TaxRegistration(id=("VA", "DE000000000"))
+    )
 
     doc.trade.agreement.seller_order.issue_date_time = datetime.now(timezone.utc)
     doc.trade.agreement.buyer_order.issue_date_time = datetime.now(timezone.utc)
@@ -48,7 +52,7 @@ def invoice_document(request):
     li.agreement.gross.amount = Decimal("999.00")
     li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62")  # C62 == pieces
     li.agreement.net.amount = Decimal("999.00")
-    li.agreement.net.basis_quantity = (Decimal("999.00"), "EUR")
+    li.agreement.net.basis_quantity = (Decimal("999.00"), "C62")
     li.delivery.billed_quantity = (Decimal("1.0000"), "C62")  # C62 == pieces
     li.settlement.trade_tax.type_code = "VAT"
     li.settlement.trade_tax.category_code = "E"
@@ -68,7 +72,7 @@ def invoice_document(request):
     doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00")
     doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00")
     doc.trade.settlement.monetary_summation.tax_basis_total = Decimal("999.00")
-    doc.trade.settlement.monetary_summation.tax_total = Decimal("0.00")
+    doc.trade.settlement.monetary_summation.tax_total = (Decimal("0.00"), "EUR")
     doc.trade.settlement.monetary_summation.grand_total = Decimal("999.00")
     doc.trade.settlement.monetary_summation.due_amount = Decimal("999.00")
 
diff --git a/tests/test_minimal.py b/tests/test_minimal.py
index d21a3b4..c521d65 100644
--- a/tests/test_minimal.py
+++ b/tests/test_minimal.py
@@ -109,3 +109,20 @@ def test_invalid_invoice_exceptions(invoice_document, invoice_pdf17_bytes):
         attach_xml(invoice_pdf17_bytes, "invalid_xml_type")
 
     assert str(exc_info.value) == "Please supply XML data as bytes."
+
+
+@pytest.mark.parametrize("invoice_document", ["380"], indirect=True)
+def test_unused_name_spaces_included(invoice_document):
+    doc = invoice_document
+    doc.context.guideline_parameter.id = (
+        "urn:cen.eu:en16932:2017#conformant#urn:factur-x.eu:1p0:wrong"
+    )
+    xml = doc.serialize(schema="FACTUR-X_EXTENDED")
+    assert (
+        'xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"'
+        in xml.decode()
+    )
+    assert 'xmlns:xs="http://www.w3.org/2001/XMLSchema"' in xml.decode()
+    assert 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' in xml.decode()
+    assert "xs:" not in xml.decode()
+    assert "xsi:" not in xml.decode()