Validate electronic invoices with Saxon in Xojo

When you receive an electronic invoice in the formats ZUGFeRD, Factur-X, X-Rechnung or UBL, you may need to validate the XML file in your Xojo application. You certainly don’t want a surprise later when you get a tax audit.

You can use our DynaPDF plugin functions to extract the XML from the ZUGFeRD invoice. Once you have the XML, you can do some automated validation. We leverage the XSL and XSD files coming with the ZUGFeRD 2.3 download. They provide a way to check both the structure with the XSD file as well as the content with the XSL file. The XSL file is based on the schematron file with all the business rules, but converted to a stylesheet for using it with the XSLT function in SaxonMBS module. While the schema check makes sure you have the right nodes in the right places, the business rules define what can go in a field and how fields are related.

Step 1: Load Saxon

First load the Saxon library. You download the libraries either from our website or directly from Saxonica website. We suggest to copy the Saxon library to the some folder and load them from there in all projects while debugging. Later you may want to use a build step to copy the library into your app’s Libs folder and load them from there at runtime. Or for macOS include it in the frameworks folder within the application.

Sub LoadSaxon()
	// load library
	
	#If TargetMacOS
		Var path As String = "/Users/cs/saxon/SaxonCEE-12.9/SaxonCEE-macos-universal-12-9-0/libsaxonc-core-ee.12.9.0.dylib"
	#ElseIf TargetWindows
		Var path As String = "saxonc-core-ee.dll"
	#ElseIf TargetLinux Then
		Var path As String = "libsaxonc-core-ee.so.12.9.0"
	#Else
		// not supported
	#EndIf
	
	Var bb As Boolean = SaxonMBS.LoadLibrary(path)
	
	// we have libraries here:
	// https://www.monkeybreadsoftware.com/filemaker/files/Libs/Saxon/
	
	If bb Then
		// okay
	Else
		MessageBox "Failed to load Saxon library. Did you download it and adjust path in this example?"+EndOfLine+SaxonMBS.LoadErrorString
		Break
		Return
	End If
End Sub

Step 2: LibXML Validation

If you don’t use our Saxon classes yet, you may use the validation in our XMLValidatorMBS class. There we read the XSD file to get the schema XML. We make sure we set the current directory, so the referenced files are found. And then we call one of the Validate functions to let the validation run through. We either get back error code 0 or the error messages from LibXML. This provides a quick check for the schema.

Sub Validate()
	Var f As FolderItem = XSLFolder.Child("Factur-X_1.07.2_EN16931.xsd")
	If f <> Nil And f.Exists Then
		
		// read the XSD file into a variable
		Var b As BinaryStream = BinaryStream.Open(f)
		Var xsd As String = b.Read(b.Length, encodings.UTF8)
		
		// set current working directory, so related files can be found
		Call SetCurrentWorkingDirectoryMBS XSLFolder
		
		// invoice from the text area
		var InvoiceXML as string = InvoiceTextArea.Text
		
		// we have a subclass to collect error messages from Error/Warning events
		Var v As New XMLValidator(f)
		
		// run the validation of the invoice
		Var e As Integer = v.ValidateString(InvoiceXML)
		
		// show errors if we got one
		If e = 0 Then
			MessageBox "Okay"
			ResultTextArea.Text = "Okay"
		Else
			ResultTextArea.Text = Join(v.Errors, EndOfLine)
		end if
	Else
		MessageBox "Factur-X_1.07.2_EN16931.xsd file not found!?"
	End If
End Sub

Step 3: Saxon Validation

We do the same schema validation as above, but this time using the Saxon classes. First we load the schema file, set the directory to find the related files and run the validation. The SaxonMBS.Validate function can return true on success or false in case of a problem. It may also raise an exception. But more details are available as a XML based report with ValidationReport function. We prepared a XML stylesheet to convert the XML from the report to HTML and show it in web viewer. This needs a Saxon EEV license from us.

Please note that this uses the simple Validate() function. If you need more options, please check the SchemaValidatorMBS class.

Sub ValidateStructure()
	//  Validate the structure of the invoice against the schema file
	
	Var SchemaFile As FolderItem = XSLFolder.Child("Factur-X_1.07.2_EN16931.xsd")
	If SchemaFile <> Nil And SchemaFile.Exists Then
		
		Var b As BinaryStream = BinaryStream.Open(SchemaFile)
		Var schemaXML As String = b.Read(b.Length, encodings.UTF8)
		
		// set current working directory, so related files can be found
		
		SaxonMBS.CWD = XSLFolder.NativePath
		
		//  run validation
		
		Var InvoiceXML As String = InvoiceTextArea.Text
		
		try
			Var r As Boolean = SaxonMBS.Validate(InvoiceXML, schemaXML)
			
		Catch s As SaxonExceptionMBS
			// failed to validate
			System.DebugLog s.Message
		end try
		
		Var report As String = SaxonMBS.ValidationReport
		
		ResultTextArea.Text = report
		
		Var html As String
		
		If report.len > 0 Then
			html = SaxonMBS.XSLT(report, XSLT2)
		Else
			html = "<p>Failed. No EE-V license?</p>"
		end If
		
		HTMLViewer1.LoadPage html, XSLFolder
	Else
		
		MessageBox "Factur-X_1.07.2_EN16931.xsd file not found!?"
		
	End If
End Sub 

For a test we duplicated the ID row and the schema detects that:

Step 4: Validate content with business rules

With ZUGFeRD we get the Schematron file with the business rules and the matching XML stylesheet for use with SaxonMBS XSLT function. We load the XSL file from the data folder.

We check how many rules we have. Currently over 400 rules are defined. We perform the transformation and get back a XML file. There you can lookup how many asserts are in the report. We prepared a XSLT to convert the report to a HTML and show that in a web viewer. For the report to show up before our dialog box reports the number of errors, we need to have a small pause. The pause allows the web viewer to show the content. This needs a Saxon PE or EEV license from us.

Sub ValidateContent()
	// Validate Content With Saxon In file Saxon ZUGFeRD Validation
	
	// We read the XSLT needed To Do validation
	Var f As FolderItem = XSLFolder.Child("FACTUR-X_EN16931.xslt")
	If f <> Nil And f.Exists Then
		
		Var b As BinaryStream = BinaryStream.Open(f)
		Var xslt As String = b.Read(b.Length, encodings.UTF8)
		
		// check count If you like To know how many rules you have
		Var countRules As Integer = xslt.CountFields("<svrl:failed-assert")-1
		
		// we need To set where To find related files. Some functions don't work with paths containing spaces
		SaxonMBS.CWD = XSLFolder.NativePath
		
		// perform check
		Var InvoiceXML As String = InvoiceTextArea.Text
		Var report As String = SaxonMBS.XSLT(InvoiceXML, xslt)
		
		ResultTextArea.Text = report
		
		if report <> "" then
			
			// check count If you like To know how many rules you have
			Var countFailed As Integer = report.CountFields("<svrl:failed-assert")-1
			
			// convert report To html
			Var html As String = SaxonMBS.XSLT(report, XSLT1)
			
			HTMLViewer1.LoadPage html, XSLFolder
			
			MessageBox  "Check completed." + EndOfLine + EndOfLine + countFailed.ToString + " of " + countRules.tostring + " rules failed."
			
		End If
		
		
	Else
		MessageBox "Failed to find FACTUR-X_EN16931.xslt file."
	end if
End Sub

For a test we duplicated the ID row and changed the invoice type and the business rules find these changes:

Please try this when you have time. We will include the project files (without the data files) with future MBS Xojo Plugins versions and update them whenever needed. Our Saxon classes are in the MBS Xojo XML Plugin.

1 Like