README example generates invalid invoice
Currently, the invoice generation example in the README does not work, there's an open PR to fix it: https://github.com/pretix/python-drafthorse/pull/89
But even after applying that PR, the generated invoice does not appear to be compliant. Mustang shows a large list of errors:
[main] INFO com.helger.schematron.xslt.SchematronResourceXSLTCache - Compiling XSLT instance [cpPath=/xslt/ZF_233/FACTUR-X_EXTENDED.xslt; urlResolved=true; URL=jar:file:/tmp/Mustang-CLI-2.19.1.jar!/xslt/ZF_233/FACTUR-X_EXTENDED.xslt]
[main] INFO com.helger.schematron.api.xslt.AbstractSchematronXSLTBasedResource - Applying Schematron XSLT on XML instance
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-FXEXT-CO-15]-If Invoice Total VAT amount (BT-110) ,where currency (BT-110-0) is equal to BT-5, is present, then the Absolute Value of (Invoice total amount with VAT (BT-112) - Invoice total amount without VAT (BT-109) - Invoice total VAT amount (BT-110)) <= 0,01 * (Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charges amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272). Else, Invoice total amount with VAT (BT-112) is equal to Invoice total amount without VAT (BT-109).
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-FXEXT-CO-15]-If Invoice Total VAT amount (BT-110) ,where currency (BT-110-0) is equal to BT-5, is present, then the Absolute Value of (Invoice total amount with VAT (BT-112) - Invoice total amount without VAT (BT-109) - Invoice total VAT amount (BT-110)) <= 0,01 * (Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charges amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272). Else, Invoice total amount with VAT (BT-112) is equal to Invoice total amount without VAT (BT-109). [ID FX-SCH-A-000307] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-FXEXT-AE-08]-In a VAT breakdown (BG-23) where VAT category code (BT-118) is equal to “AE” ("Reverse Charge"), Absolute Value of (VAT category taxable amount (BT-116) - ∑ Invoice line net amounts (BT-131) + Σ Document level allowance amounts (BT-92) - Σ Document level charge amounts (BT-99) - Σ Logistics Service fee amounts (BT-x-272)) <= 0,01 * ((Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charge amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272)), where the VAT category code (BT-151, BT-95, BT-102, BT-X-273) is "Reversed Charge" (AE).
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-FXEXT-AE-08]-In a VAT breakdown (BG-23) where VAT category code (BT-118) is equal to “AE” ("Reverse Charge"), Absolute Value of (VAT category taxable amount (BT-116) - ∑ Invoice line net amounts (BT-131) + Σ Document level allowance amounts (BT-92) - Σ Document level charge amounts (BT-99) - Σ Logistics Service fee amounts (BT-x-272)) <= 0,01 * ((Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charge amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272)), where the VAT category code (BT-151, BT-95, BT-102, BT-X-273) is "Reversed Charge" (AE). [ID FX-SCH-A-000308] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-CO-25]-In case the Amount due for payment (BT-115) is positive, either the Payment due date (BT-9) or the Payment terms (BT-20) shall be present.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-CO-25]-In case the Amount due for payment (BT-115) is positive, either the Payment due date (BT-9) or the Payment terms (BT-20) shall be present. [ID FX-SCH-A-000155] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-10]-An Invoice shall contain the Buyer postal address (BG-8).
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-10]-An Invoice shall contain the Buyer postal address (BG-8). [ID FX-SCH-A-000156] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-11]-The Buyer postal address shall contain a Buyer country code (BT-55).
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-11]-The Buyer postal address shall contain a Buyer country code (BT-55). [ID FX-SCH-A-000157] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-E-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is “Exempt from VAT” shall contain exactly one VAT breakdown (BG-23) with the VAT category code (BT-118) equal to "Exempt from VAT".
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-E-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is “Exempt from VAT” shall contain exactly one VAT breakdown (BG-23) with the VAT category code (BT-118) equal to "Exempt from VAT". [ID FX-SCH-A-000256] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
[BR-AE-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is "Reverse charge" shall contain in the VAT breakdown (BG-23) exactly one VAT category code (BT-118) equal with "VAT reverse charge".
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
[BR-AE-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is "Reverse charge" shall contain in the VAT breakdown (BG-23) exactly one VAT category code (BT-118) equal with "VAT reverse charge". [ID FX-SCH-A-000257] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
Value of '@unitCode' is not allowed.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
Value of '@unitCode' is not allowed. [ID FX-SCH-A-000275] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
Element 'ram:PostalTradeAddress' must occur exactly 1 times.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
Element 'ram:PostalTradeAddress' must occur exactly 1 times. [ID FX-SCH-A-000032] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
Element 'ram:IssuerAssignedID' must occur exactly 1 times.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
Element 'ram:IssuerAssignedID' must occur exactly 1 times.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] INFO org.mustangproject.validator.XMLValidator - FailedAssert
Element 'ram:IssuerAssignedID' must occur exactly 1 times.
[main] ERROR org.mustangproject.validator.ZUGFeRDValidator - Error 4:
Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)
[main] WARN org.mustangproject.validator.ZUGFeRDValidator - Warning 10: Arithmetical issue:Payable total in XML is 999.00, but calculated total is 1.00 with tax basis 1.00 and with positions 1.00 = 1.00
[main] INFO org.mustangproject.validator.ZUGFeRDValidator - Parsed PDF:absent XML:invalid Signature:null Checksum:0D4BCAD997373799B42293F6CACBECDC9716CC6A Profile:urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended Version:2 Took:2554ms Errors:[4,4,4,4,4,4,4,4,4,4,4,4,10] ErrorIDs: [FX-SCH-A-000307,FX-SCH-A-000308,FX-SCH-A-000155,FX-SCH-A-000156,FX-SCH-A-000157,FX-SCH-A-000256,FX-SCH-A-000257,FX-SCH-A-000275,FX-SCH-A-000032,FX-SCH-A-000029,FX-SCH-A-000029,FX-SCH-A-000029]
<?xml version="1.0" encoding="UTF-8"?>
<validation filename="ex.xml" datetime="2025-10-29 10:49:25">
<xml>
<info>
<version>2</version>
<profile>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</profile>
<validator version="2.19.1"/>
<rules>
<fired>71</fired>
<failed>12</failed>
</rules>
<duration unit="ms">2449</duration>
</info>
<messages>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedTradeSettlementHeaderMonetarySummation[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="for $Currency in ../ram:InvoiceCurrencyCode, $BT109 in xs:decimal(ram:TaxBasisTotalAmount), $BT110 in xs:decimal(ram:TaxTotalAmount[@currencyID=$Currency]), $BT112 in xs:decimal(ram:GrandTotalAmount), $nbTaxTotalAmountInvoiceCurrency in count (ram:TaxTotalAmount[@currencyID=$Currency] ), $nbLineItems in xs:decimal(count(../../ram:IncludedSupplyChainTradeLineItem)), $nbAllowanceItems in xs:decimal(count(../ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator='false'])), $nbChargeItems in xs:decimal(count(../ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator='true']) + count(../ram:SpecifiedLogisticsServiceCharge)), $tolerance in xs:decimal(0.01), $maxTolerance in $tolerance * ($nbLineItems + $nbAllowanceItems + $nbChargeItems), $diff in xs:decimal($BT112 - $BT110 - $BT109), $abs in xs:decimal(abs($diff)) return ($abs le $maxTolerance and $nbTaxTotalAmountInvoiceCurrency eq 1) or ($BT109 eq $BT112 and $nbTaxTotalAmountInvoiceCurrency ne 1)">[BR-FXEXT-CO-15]-If Invoice Total VAT amount (BT-110) ,where currency (BT-110-0) is equal to BT-5, is present, then the Absolute Value of (Invoice total amount with VAT (BT-112) - Invoice total amount without VAT (BT-109) - Invoice total VAT amount (BT-110)) <= 0,01 * (Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charges amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272). Else, Invoice total amount with VAT (BT-112) is equal to Invoice total amount without VAT (BT-109). [ID FX-SCH-A-000307] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ApplicableTradeTax[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:CategoryCode[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="for $basisAmount in xs:decimal(../ram:BasisAmount), $lineAmount in xs:decimal(round(sum(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement[ram:ApplicableTradeTax/ram:CategoryCode = 'AE']/ram:SpecifiedTradeSettlementLineMonetarySummation/xs:decimal(ram:LineTotalAmount)) * 100) div 100), $chargeAmount in xs:decimal(round(sum(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator=true() and ram:CategoryTradeTax/ram:CategoryCode='AE']/xs:decimal(ram:ActualAmount)) * 100) div 100), $logisticChargeAmount in xs:decimal(round(sum(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedLogisticsServiceCharge[ram:AppliedTradeTax/ram:CategoryCode='AE']/xs:decimal(ram:AppliedAmount)) * 100) div 100), $allowanceAmount in xs:decimal(round(sum(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator=false() and ram:CategoryTradeTax/ram:CategoryCode='AE']/xs:decimal(ram:ActualAmount)) * 100) div 100), $calculatedAmount in xs:decimal($lineAmount + $chargeAmount + $logisticChargeAmount - $allowanceAmount), $nbLineItems in xs:decimal(count(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem[ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode = 'AE'])), $nbAllowancesOrCharges in xs:decimal(count(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge[ram:CategoryTradeTax/ram:CategoryCode='AE'])), $nbLogisticCharges in xs:decimal(count(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedLogisticsServiceCharge[ram:AppliedTradeTax/ram:CategoryCode='AE'])), $tolerance in xs:decimal(0.01), $maxTolerance in $tolerance * ($nbLineItems + $nbAllowancesOrCharges + $nbLogisticCharges), $diff in xs:decimal($basisAmount - $calculatedAmount), $abs in xs:decimal(abs($diff)) return $abs le $maxTolerance">[BR-FXEXT-AE-08]-In a VAT breakdown (BG-23) where VAT category code (BT-118) is equal to “AE” ("Reverse Charge"), Absolute Value of (VAT category taxable amount (BT-116) - ∑ Invoice line net amounts (BT-131) + Σ Document level allowance amounts (BT-92) - Σ Document level charge amounts (BT-99) - Σ Logistics Service fee amounts (BT-x-272)) <= 0,01 * ((Number of line net amounts (BT-131) + Number of Document level allowance amounts (BT-92) + Number of Document level charge amounts (BT-99) + Number of Logistics Service fee amounts (BT-X-272)), where the VAT category code (BT-151, BT-95, BT-102, BT-X-273) is "Reversed Charge" (AE). [ID FX-SCH-A-000308] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" criterion="(number(//ram:DuePayableAmount) > 0 and ((//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime) or (//ram:SpecifiedTradePaymentTerms/ram:Description))) or not(number(//ram:DuePayableAmount) > 0)">[BR-CO-25]-In case the Amount due for payment (BT-115) is positive, either the Payment due date (BT-9) or the Payment terms (BT-20) shall be present. [ID FX-SCH-A-000155] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" criterion="//ram:BuyerTradeParty/ram:PostalTradeAddress">[BR-10]-An Invoice shall contain the Buyer postal address (BG-8). [ID FX-SCH-A-000156] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" criterion="//ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CountryID!=''">[BR-11]-The Buyer postal address shall contain a Buyer country code (BT-55). [ID FX-SCH-A-000157] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" criterion="(count(//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='E'])=0 and count(//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='E'])=0 and count(//ram:CategoryTradeTax[ram:CategoryCode='E'])=0) or ( count(//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='E'])=1 and (exists(//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='E']) or exists(//ram:CategoryTradeTax[ram:CategoryCode='E'])))">[BR-E-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is “Exempt from VAT” shall contain exactly one VAT breakdown (BG-23) with the VAT category code (BT-118) equal to "Exempt from VAT". [ID FX-SCH-A-000256] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" criterion="(count(//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='AE'])=0 and count(//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='AE'])=0 and count(//ram:CategoryTradeTax[ram:CategoryCode='AE'])=0) or ( count(//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='AE'])=1 and (exists(//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode='AE']) or exists(//ram:CategoryTradeTax[ram:CategoryCode='AE'])))">[BR-AE-01]-An Invoice that contains an Invoice line (BG-25), a Document level allowance (BG-20) or a Document level charge (BG-21) where the VAT category code (BT-151, BT-95 or BT-102) is "Reverse charge" shall contain in the VAT breakdown (BG-23) exactly one VAT category code (BT-118) equal with "VAT reverse charge". [ID FX-SCH-A-000257] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:IncludedSupplyChainTradeLineItem[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedLineTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:NetPriceProductTradePrice[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:BasisQuantity[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="string-length($codeValue11)=0 or document('FACTUR-X_EXTENDED_codedb.xml')/codedb/cl[@id=11]/enumeration[@value=$codeValue11]">Value of '@unitCode' is not allowed. [ID FX-SCH-A-000275] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:BuyerTradeParty[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="count(ram:PostalTradeAddress)=1">Element 'ram:PostalTradeAddress' must occur exactly 1 times. [ID FX-SCH-A-000032] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SellerOrderReferencedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="count(ram:IssuerAssignedID)=1">Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:BuyerOrderReferencedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="count(ram:IssuerAssignedID)=1">Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<error type="4" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:UltimateCustomerOrderReferencedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" criterion="count(ram:IssuerAssignedID)=1">Element 'ram:IssuerAssignedID' must occur exactly 1 times. [ID FX-SCH-A-000029] from /xslt/ZF_233/FACTUR-X_EXTENDED.xslt)</error>
<warning type="10">Arithmetical issue:Payable total in XML is 999.00, but calculated total is 1.00 with tax basis 1.00 and with positions 1.00 = 1.00</warning>
</messages>
<summary status="invalid"/>
</xml>
<messages></messages>
<summary status="invalid"/>
</validation>
Hi @hannob, do you think the example really needs to pass the mustang validation? I used the sample as a starting point and adjusted the structure to match our invoice format. From there, I fixed one validation error after another.
I tried to do that, and found it difficult to parse some of the errors generated by the validation and understand what they're trying to tell me.. I think a working example to create an EN16931 compliant invoice would be very helpful. Whether in the README or somewhere else.
The following example, using the basic profile, and a normal VAT, yields 0 errors with saxon, but still many errors tested with https://erechnungsvalidator.service-bw.de/.
I tested it using:
java -jar /usr/share/java/Saxon-HE.jar -s:factur-x.xml -xsl:/home/mdk/Downloads/_Factur-X\ 1.07.3\ Zugferd\ 2.3.3\ -\ 2025\ 05\ 15\ -\ FINAL\ FR/4.\ FACTUR-X_1.07.3_XSD_SCHEMATRON_2025-05-15/2.\ Factur-X_1.07.3_BASIC/_XSLT_BASIC/FACTUR-X_BASIC.xslt -o:output.xml && xmllint --format --xpath "//*[local-name()='failed-assert']" output.xml
from datetime import date, datetime, timedelta, timezone
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.payment import PaymentTerms
from drafthorse.models.tradelines import LineItem
from drafthorse.pdf import attach_xml
# Build data structure
doc = Document()
doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic"
doc.header.id = "RE1337"
doc.header.type_code = "380"
doc.header.name = "RECHNUNG"
doc.header.issue_date_time = date.today()
doc.header.languages.add("de")
doc.header.notes.add(IncludedNote(content="Test Note 1"))
doc.trade.agreement.seller.name = "Lieferant GmbH"
doc.trade.settlement.payee.name = "Lieferant GmbH"
doc.trade.agreement.buyer.name = "Kunde GmbH"
doc.trade.agreement.buyer.address.country_id = "DE"
doc.trade.settlement.invoicee.name = "Kunde GmbH"
doc.trade.settlement.currency_code = "EUR"
doc.trade.settlement.payment_means.type_code = "ZZZ"
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.settlement.advance_payment.received_date = datetime.now(timezone.utc)
doc.trade.agreement.customer_order.issue_date_time = datetime.now(timezone.utc)
li = LineItem()
li.document.line_id = "1"
li.product.name = "Rainbow"
li.agreement.net.amount = Decimal("999")
li.agreement.net.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.agreement.gross.amount = Decimal("1198.8")
li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.delivery.billed_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.settlement.trade_tax.type_code = "VAT"
li.settlement.trade_tax.category_code = "S"
li.settlement.trade_tax.rate_applicable_percent = Decimal("20.00")
li.settlement.monetary_summation.total_amount = Decimal("999.00")
doc.trade.items.add(li)
trade_tax = ApplicableTradeTax()
trade_tax.calculated_amount = Decimal("199.80")
trade_tax.basis_amount = Decimal("999.00")
trade_tax.type_code = "VAT"
trade_tax.category_code = "S"
trade_tax.rate_applicable_percent = Decimal("20.00")
doc.trade.settlement.trade_tax.add(trade_tax)
doc.trade.settlement.monetary_summation.line_total = Decimal("999.00")
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("199.80")
doc.trade.settlement.monetary_summation.grand_total = Decimal("1198.8")
doc.trade.settlement.monetary_summation.due_amount = Decimal("1198.8")
terms = PaymentTerms()
terms.due = datetime.now(timezone.utc) + timedelta(days=30)
doc.trade.settlement.terms.add(terms)
# Generate XML file
xml = doc.serialize(schema="FACTUR-X_EXTENDED")
# Attach XML to an existing PDF.
# Note that the existing PDF should be compliant to PDF/A-3!
# You can validate this here: https://www.pdf-online.com/osa/validate.aspx
with open("input.pdf", "rb") as original_file:
new_pdf_bytes = attach_xml(original_file.read(), xml)
with open("output.pdf", "wb") as f:
f.write(new_pdf_bytes)
The following example, using the basic profile, and a normal VAT, yields 0 errors with saxon, but still many errors tested with https://erechnungsvalidator.service-bw.de/.
Your example is, I believe, valid according to Zugferd, but invalid according to EN16931. (Yeah, the fact that this is even possible is interesting.)
Errors with the CII EN16931 schematron/xslt (see also my test script):
CII validation errors:
[
{
"@test": "every $Currency in rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode satisfies ( count ( rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount[@currencyID=$Currency] ) eq 1 and (//ram:SpecifiedTradeSettlementHeaderMonetarySummation/xs:decimal(ram:GrandTotalAmount) = round( (//ram:SpecifiedTradeSettlementHeaderMonetarySummation/xs:decimal(ram:TaxBasisTotalAmount) + (//ram:SpecifiedTradeSettlementHeaderMonetarySummation/xs:decimal(ram:TaxTotalAmount[@currencyID=$Currency]))) * 10 * 10) div 100)) or (//ram:SpecifiedTradeSettlementHeaderMonetarySummation/xs:decimal(ram:GrandTotalAmount) = (//ram:SpecifiedTradeSettlementHeaderMonetarySummation/xs:decimal(ram:TaxBasisTotalAmount)))",
"@id": "BR-CO-15",
"@flag": "fatal",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]",
"svrl:text": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110)."
},
{
"@test": "(ram:Name) and (not(ram:Name = ../../ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name) and not(ram:ID = ../../ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:ID) and not(ram:SpecifiedLegalOrganization/ram:ID = ../../ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID))",
"@id": "BR-17",
"@flag": "fatal",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:PayeeTradeParty[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]",
"svrl:text": "[BR-17]-The Payee name (BT-59) shall be provided in the Invoice, if the Payee (BG-10) is different from the Seller (BG-4)."
},
{
"@test": "not(ram:Name)",
"@id": "CII-SR-013",
"@flag": "warning",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ExchangedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]",
"svrl:text": "[CII-SR-013] - Name should not be present"
},
{
"@test": "not(ram:LanguageID)",
"@id": "CII-SR-019",
"@flag": "warning",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ExchangedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]",
"svrl:text": "[CII-SR-019] - LanguageID should not be present"
},
{
"@test": "not(ram:UltimateCustomerOrderReferencedDocument)",
"@id": "CII-SR-448",
"@flag": "warning",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]",
"svrl:text": "[CII-SR-448] - UltimateCustomerOrderReferencedDocument should not be present"
},
{
"@test": "not(ram:FormattedIssueDateTime) or self::ram:InvoiceReferencedDocument",
"@id": "CII-DT-027",
"@flag": "fatal",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:UltimateCustomerOrderReferencedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]",
"svrl:text": "[CII-DT-027] - FormattedIssueDateTime should not be present"
},
{
"@test": "not(ram:InvoiceeTradeParty)",
"@id": "CII-SR-351",
"@flag": "warning",
"@location": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]",
"svrl:text": "[CII-SR-351] - InvoiceeTradeParty should not be present"
}
]
I modified it slightly
from datetime import date, datetime, timedelta, timezone
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.payment import PaymentTerms
from drafthorse.models.tradelines import LineItem
from drafthorse.pdf import attach_xml
# Build data structure
doc = Document()
doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017"
doc.header.id = "RE1337"
doc.header.type_code = "380"
# doc.header.name = "RECHNUNG"
doc.header.issue_date_time = date.today()
#doc.header.languages.add("de")
doc.header.notes.add(IncludedNote(content="Test Note 1"))
doc.trade.agreement.seller.name = "Lieferant GmbH"
#doc.trade.settlement.payee.name = "Lieferant GmbH"
doc.trade.agreement.buyer.name = "Kunde GmbH"
doc.trade.agreement.buyer.address.country_id = "DE"
#doc.trade.settlement.invoicee.name = "Kunde GmbH"
doc.trade.settlement.currency_code = "EUR"
doc.trade.settlement.payment_means.type_code = "ZZZ"
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.settlement.advance_payment.received_date = datetime.now(timezone.utc)
li = LineItem()
li.document.line_id = "1"
li.product.name = "Rainbow"
li.agreement.net.amount = Decimal("999")
li.agreement.net.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.agreement.gross.amount = Decimal("1198.8")
li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.delivery.billed_quantity = (Decimal("1.0000"), "C62") # C62 == unit
li.settlement.trade_tax.type_code = "VAT"
li.settlement.trade_tax.category_code = "S"
li.settlement.trade_tax.rate_applicable_percent = Decimal("20.00")
li.settlement.monetary_summation.total_amount = Decimal("999.00")
doc.trade.items.add(li)
trade_tax = ApplicableTradeTax()
trade_tax.calculated_amount = Decimal("199.80")
trade_tax.basis_amount = Decimal("999.00")
trade_tax.type_code = "VAT"
trade_tax.category_code = "S"
trade_tax.rate_applicable_percent = Decimal("20.00")
doc.trade.settlement.trade_tax.add(trade_tax)
doc.trade.settlement.monetary_summation.line_total = Decimal("999.00")
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("199.80"), "EUR")
doc.trade.settlement.monetary_summation.grand_total = Decimal("1198.8")
doc.trade.settlement.monetary_summation.due_amount = Decimal("1198.8")
terms = PaymentTerms()
terms.due = datetime.now(timezone.utc) + timedelta(days=30)
doc.trade.settlement.terms.add(terms)
# Generate XML file
xml = doc.serialize(schema="FACTUR-X_EXTENDED")
print(xml.decode())
exit(0)
# Attach XML to an existing PDF.
# Note that the existing PDF should be compliant to PDF/A-3!
# You can validate this here: https://www.pdf-online.com/osa/validate.aspx
with open("input.pdf", "rb") as original_file:
new_pdf_bytes = attach_xml(original_file.read(), xml)
with open("output.pdf", "wb") as f:
f.write(new_pdf_bytes)
It now passes java -jar /usr/share/java/Saxon-HE.jar -s:factur-x.xml -xsl:EN16931-CII-validation.xslt -o:output.xml locally.
It also passes your eival:
$ ./eival ../python-drafthorse/factur-x.xml
$ ../python-drafthorse/factur-x.xml valid
But it still raises issues on https://erechnungsvalidator.service-bw.de/ :
| Pos | Code | Adj. Grad | Text |
|---|---|---|---|
| val-sch.2.1 | PEPPOL-EN16931-R001 | information | Business process MUST be provided. Pfad: /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext[1] |
| val-sch.2.2 | PEPPOL-EN16931-R020 | information | Seller electronic address MUST be provided Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:SellerTradeParty[1] |
| val-sch.2.3 | PEPPOL-EN16931-R010 | information | Buyer electronic address MUST be provided Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:BuyerTradeParty[1] |
| val-sch.2.4 | PEPPOL-EN16931-R046 | information | Item net price MUST equal (Gross price - Allowance amount) when gross price is provided. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:IncludedSupplyChainTradeLineItem[1]/ram:SpecifiedLineTradeAgreement[1]/ram:GrossPriceProductTradePrice[1] |
| val-sch.2.5 | BR-DE-1 | error | [BR-DE-1] Eine Rechnung (INVOICE) muss Angaben zu "PAYMENT INSTRUCTIONS" (BG-16) enthalten. Pfad: /rsm:CrossIndustryInvoice |
| val-sch.2.6 | BR-DE-15 | error | [BR-DE-15] Das Element "Buyer reference" (BT-10) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice |
| val-sch.2.7 | BR-DE-21 | warning | [BR-DE-21] Das Element "Specification identifier" (BT-24) soll syntaktisch der Kennung des Standards XRechnung entsprechen. Pfad: /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext[1] |
| val-sch.2.8 | BR-DE-2 | error | [BR-DE-2] Die Gruppe "SELLER CONTACT" (BG-6) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:SellerTradeParty[1] |
| val-sch.2.9 | BR-DE-3 | error | [BR-DE-3] Das Element "Seller city" (BT-37) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:SellerTradeParty[1]/ram:PostalTradeAddress[1] |
| val-sch.2.10 | BR-DE-4 | error | [BR-DE-4] Das Element "Seller post code" (BT-38) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:SellerTradeParty[1]/ram:PostalTradeAddress[1] |
| val-sch.2.11 | BR-DE-8 | error | [BR-DE-8] Das Element "Buyer city" (BT-52) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:BuyerTradeParty[1]/ram:PostalTradeAddress[1] |
| val-sch.2.12 | BR-DE-9 | error | [BR-DE-9] Das Element "Buyer post code" (BT-53) muss übermittelt werden. Pfad: /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction[1]/ram:ApplicableHeaderTradeAgreement[1]/ram:BuyerTradeParty[1]/ram:PostalTradeAddress[1] |
Interesting fact, https://erechnungsvalidator.service-bw.de/ gives a table about the errors reported for each version of the spec:
| Prüfschritt | Fehler | Warnungen | Informationen |
|---|---|---|---|
| XML Schema for UN/CEFACT XML (SCRDM - CII uncoupled) (val-xsd) | 0 | 0 | 0 |
| Schematron rules for EN16931 (CII) (val-sch.1) | 0 | 0 | 0 |
| Schematron rules for CIUS XRechnung (CII) (val-sch.2) | 7 | 5 | 0 |
| (val-xml) | 0 | 0 | 0 |
So at lease we all agree: this example passes EN16931. I don't know CIUS XRechnung (CII) (val-sch.2) yet but I bet we can commit it like this, to at least have a valid example using EN16931?