If you want to help or get the latest version: ZUV has been completely merged into Mustang 2.0.
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.
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
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.
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>
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);
- Redirecting stderr away (some logging messages might otherwise disturb XML well formedness)
- Escaping any file names in case you use original file names at all (apart from security concerns please take into account that they might contain spaces)
- Signal java to use UTF-8 even when otherwise it would not: You might run into trouble with XML files starting with a BOM otherwise and when you exec, keep in mind that you lose all env vars.
Permissive Open Source APL2, see LICENSE
section | meaning |
---|---|
1 | file not found |
2 | additional data schema validation fails |
3 | xml data not found |
4 | schematron rule failed |
5 | file too small |
6 | VeraPDFException |
7 | IOException PDF |
8 | File does not look like PDF nor XML (contains neither %PDF nor <?xml) |
9 | IOException XML |
11 | XMP Metadata: ConformanceLevel not found |
12 | XMP Metadata: ConformanceLevel contains invalid value |
13 | XMP Metadata: DocumentType not found |
14 | XMP Metadata: DocumentType invalid |
15 | XMP Metadata: Version not found |
16 | XMP Metadata: Version contains invalid value |
18 | schema validation failed |
19 | XMP Metadata: DocumentFileName contains invalid value |
20 | not a pdf |
21 | XMP Metadata: DocumentFileName not found") |
22 | generic XML validation exception |
23 | Not a PDF/A-3 |
24 | Issues in CEN EN16931 Schematron Check |
25 | Unsupported profile type |
26 | No rules matched, XML to minimal? |
27 | XRechnung schematron validation |
see the history file
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"]
}
}
Jochen Staerk "Mustangproject Chief ZUGFeRD amatuer" jochen@zugferd.org