Home

Awesome

Merge into Mustang

If you want to help or get the latest version: ZUV has been completely merged into Mustang 2.0.

ZUV

ZUV (ZUgferd+VeraPDF) was the project name of an open-source e-invoice validator for the ZUGFeRD/Factur-X standard.

It checks both PDF/A-3 compliance (based on VeraPDF) and ZUGFeRD version 1 respectively 2/2.1 XMLs for correctness. The XML check is done by validating against the official ZUGFeRD schematron file for v1 and v2.1 and additionally the EN16931 UN/CEFACT SCRDM v16B uncoupled schematron from the CEN.

Build

In the pom.xml directory compile the jar with ./mvnw clean package

To prepare the schematron files they have to be converted to XSLT files, which is done with a XSLT transformation itself. Get Saxon and the XSLT to convert schematron to XSLT and run java -jar ./saxon9he.jar -o:minimum.xsl -s:zugferd2p0_basicwl_minimum.sch ./schematron_to_xslt/iso-schematron-xslt2/iso_svrl_for_xslt1.xsl to do so.

To create a new en16931 run git clone https://github.com/ConnectingEurope/eInvoicing-EN16931.git mvn -f pom-xslt.xml package

Run

To check a file for ZUGFeRD conformance use

java -jar ZUV-0.9.0.jar --action validate -f <filename of ZUGFeRD PDF.pdf>

You can provide either the complete PDF which will be checked for XML and PDF correctness, or just a XML file, which of course will only be checked for XML correctness.

Output

A valid file looks like this

<?xml version="1.0" encoding="UTF-8"?>

<validation filename="Facture_UE_MINIMUM.pdf" datetime="2020-03-23 12:34:54">
  <pdf> 
    <report> 
      <buildInformation> 
        <releaseDetails id="core" version="1.12.1" buildDate="2018-05-08T18:57:00+02:00"/>  
        <releaseDetails id="validation-model" version="1.12.1" buildDate="2018-05-08T20:39:00+02:00"/> 
      </buildInformation>  
      <jobs> 
        <job> 
          <item size="101181"> 
            <name>/Users/jstaerk/workspace/zugferd/helper_files/../foreign_samples/fx/Facture_UE_MINIMUM.pdf</name> 
          </item>  
          <validationReport profileName="PDF/A-3B validation profile" statement="PDF file is compliant with Validation Profile requirements." isCompliant="true"> 
            <details passedRules="123" failedRules="0" passedChecks="11082" failedChecks="0"/> 
          </validationReport>  
          <duration start="1584963295227" finish="1584963296646">00:00:01.419</duration> 
        </job> 
      </jobs>  
      <batchSummary totalJobs="1" failedToParse="0" encrypted="0"> 
        <validationReports compliant="1" nonCompliant="0" failedJobs="0">1</validationReports>  
        <featureReports failedJobs="0">0</featureReports>  
        <repairReports failedJobs="0">0</repairReports>  
        <duration start="1584963294997" finish="1584963296679">00:00:01.682</duration> 
      </batchSummary> 
    </report>  
    <info>
      <signature>Factur/X Python</signature>
      <duration unit="ms">2361</duration>
    </info>
    <summary status="valid"/>
  </pdf>  
  <xml>
    <info>
      <version>2</version>
      <profile>urn:factur-x.eu:1p0:minimum</profile>
      <validator version="0.8.4-RESTRICTED"/>
      <rules>
        <fired>26</fired>
        <failed>0</failed>
      </rules>
      <duration unit="ms">2772</duration>
    </info>
    <summary status="valid"/>
  </xml>
  <summary status="valid"/>
</validation>

Invalid files with errors e.g. in the XML part can look like this

<?xml version="1.0" encoding="UTF-8"?>

<validation filename="Facture_UE_EXTENDED.pdf" datetime="2020-03-23 12:34:43">
  <pdf> 
    <report> 
      <buildInformation> 
        <releaseDetails id="core" version="1.12.1" buildDate="2018-05-08T18:57:00+02:00"/>  
        <releaseDetails id="validation-model" version="1.12.1" buildDate="2018-05-08T20:39:00+02:00"/> 
      </buildInformation>  
      <jobs> 
        <job> 
          <item size="109172"> 
            <name>/Users/jstaerk/workspace/zugferd/helper_files/../foreign_samples/fx/Facture_UE_EXTENDED.pdf</name> 
          </item>  
          <validationReport profileName="PDF/A-3B validation profile" statement="PDF file is compliant with Validation Profile requirements." isCompliant="true"> 
            <details passedRules="123" failedRules="0" passedChecks="11082" failedChecks="0"/> 
          </validationReport>  
          <duration start="1584963283448" finish="1584963284790">00:00:01.342</duration> 
        </job> 
      </jobs>  
      <batchSummary totalJobs="1" failedToParse="0" encrypted="0"> 
        <validationReports compliant="1" nonCompliant="0" failedJobs="0">1</validationReports>  
        <featureReports failedJobs="0">0</featureReports>  
        <repairReports failedJobs="0">0</repairReports>  
        <duration start="1584963283223" finish="1584963284825">00:00:01.602</duration> 
      </batchSummary> 
    </report>  
    <info>
      <signature>Factur/X Python</signature>
      <duration unit="ms">2296</duration>
    </info>
    <summary status="valid"/>
  </pdf>  
  <xml>
    <info>
      <version>2</version>
      <profile>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</profile>
      <validator version="0.8.4-RESTRICTED"/>
      <rules>
        <fired>97</fired>
        <failed>1</failed>
      </rules>
      <duration unit="ms">8546</duration>
    </info>
    <messages>
      <error type="4" location="/*[local-name()='CrossIndustryInvoice' and namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100']/*[local-name()='SupplyChainTradeTransaction' and namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100']/*[local-name()='ApplicableHeaderTradeSettlement' and namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100']/*[local-name()='ApplicableTradeTax' and namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100']/*[local-name()='CategoryCode' and namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100']" criterion="(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString) or (../../ram:BillingSpecifiedPeriod/ram:StartDateTime) or (../../ram:BillingSpecifiedPeriod/ram:EndDateTime)">In an Invoice with a VAT breakdown (BG-23) where the VAT category code (BT-118) is "Intra-community supply" the Actual delivery date (BT-72) or the Invoicing period (BG-14) shall not be blank.</error> 
    </messages>
    <summary status="invalid"/>
  </xml>
  <messages></messages>
  <summary status="invalid"/>
</validation>

Embed

Feel free to embed this into your java software, send me a PR to use it as a library, or exec it and parse it's output to put on the web.

For exec, you might try something like

exec('java -Dfile.encoding=UTF-8 -jar /path/to/ZUV-0.9.0.jar --action validate -f '.escapeshellarg($uploadfile).' 2>/dev/null', $output);

License

Permissive Open Source APL2, see LICENSE

Codes

sectionmeaning
1file not found
2additional data schema validation fails
3xml data not found
4schematron rule failed
5file too small
6VeraPDFException
7IOException PDF
8File does not look like PDF nor XML (contains neither %PDF nor <?xml)
9IOException XML
11XMP Metadata: ConformanceLevel not found
12XMP Metadata: ConformanceLevel contains invalid value
13XMP Metadata: DocumentType not found
14XMP Metadata: DocumentType invalid
15XMP Metadata: Version not found
16XMP Metadata: Version contains invalid value
18schema validation failed
19XMP Metadata: DocumentFileName contains invalid value
20not a pdf
21XMP Metadata: DocumentFileName not found")
22generic XML validation exception
23Not a PDF/A-3
24Issues in CEN EN16931 Schematron Check
25Unsupported profile type
26No rules matched, XML to minimal?
27XRechnung schematron validation

History

see the history file

Architecture

Diagram of the architecture

Monitoring

This is a sample of a logstash config file for the ZUV log format

input {
    # stdin {}
    file {
	path => "/var/log/*.log"
	start_position => "beginning"
	sincedb_path => "/dev/null"
    }
}

filter {
    grok {
	match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[main\] %{WORD:loglevel}  ZUV.ZUGFeRDValidator - Parsed PDF:%{WORD:pdfvalidity} XML:%{WORD:xmlvalidity} Signature:%{WORD:signature} Checksum:%{WORD:checksum} Profile:%{DATA:profile} Version:%{WORD:version} Took:%{NUMBER:parseTime}" }
    }

    date {
	match => [ "timestamp" , "yyyy-MM-dd HH:mm:ss.SSS" ]
    }

}

output {
    # stdout {}
    elasticsearch {
	hosts => ["138.201.160.93:9200"]
    }
}

Authors

Jochen Staerk "Mustangproject Chief ZUGFeRD amatuer" jochen@zugferd.org