Factur-X demo

Genero Report Writer allows you to produce electronic invoice documents conforming to the Factur-X standard.

A Factur-X invoice is a PDF document that contains an embedded XML document. This XML document contains the visual invoice data in a structured way suitable for electronic processing.

The demo FacturX has been added to the OrderReport demonstration project and demonstrates a project producing electronic invoice documents conforming to the Factur-X standard. This Factur-X demo uses the EN 16931 profile.

Semantic model API

A semantic model API has been added to produce the XML document and comes in five flavours that correspond to the five Factur-X profiles:
  • Minimum
  • Basic Without Lines
  • Basic
  • EN 16931 aka. XRECHNUNG
  • Extended
This API simplifies the producing of the XML document in the following ways:
  • The produced XML always matches the profile specific XML schema provided with the specification (.xsd). It may however fail to pass the schematron validation for reasons such as the rule BR-CO-25.
  • Computed fields are automatically computed according to the specification rules. The computed fields are accessible via the API and can be printed.
  • All identifiers in the API correspond to the business terms used in the specification and not to the naming of the elements in the schema (.xsd).
  • The API is typed. All types in the API correspond to one of the 10 base types listed in the specification. In the case of the type Code, a list (enum) is provided for each type so invalid values are not specified.
  • The API is fully documented. The documentation of any class or method contains the business group number, the business term number, the description from the specification and the XPath to its location in the XML file.
  • The semantic models allow lower profiles to have less methods and 'depth' compared to the schemas. The 'depth' of the API is oriented on the semantic model, which can have up to 3 levels, not on the schema which has up to 7 levels. The ratio per profile is:
    • Minimal and Basic Without Lines: 5/2
    • Basic and EN 16931: 5/3
    • Extended:7/3
  • Besides omitting methods that are not available in a particular profile, methods for properties that can occur only once in one profile and multiple times in another are different in the simple profile compared to the more complex one. For example, the method setVATCurrency() is a method of the Invoice class in the Minimal profile while it is a method of the InvoiceTotalVATAmount class in all other profiles where an Invoice can have multiple currencies.

fgl_report_setPDFFacturXAttachment

The API call fgl_report_setPDFFacturXAttachment() has been added to embed the XML document in the PDF. For more information on the fgl_report_setPDFFacturXAttachment() API call, go to fgl_report_setPDFFacturXAttachment().

Migration from other software

Previously, when attempting to migrate source code which uses another API, it could be difficult to find which method in the previous API maps to which method in the Genero semantic API.

The new semantic model API contains a call and a command line option to create source code from a Factur-X XML file created by the previous software. When run, the source code produces the same Factur-X XML file.

$java -jar $(PATH_TO_JAR)/gre.jar com.fourjs.report.facturx.profiles.en16931.Invoice
-xmlInput files-from-other-tool/factur-x.xml
-packageName myproject.unittests.facturx
-className MigationTest -referenceFilesDirectory files-from-other-tool
-JUnitJavaOutput

Factur-X validation

One tool endorsed by the Genero Report Writer development team used for validation is the Mustang project. It provides a simple command line tool for the validation of Factur-X PDF or XML files.

Extracting a FacturX XML attachment from a PDF and navigating its semantic model

The previously mentioned command line utility from the Mustang project can be used to extract a Factur-X XML document from a PDF as follows:

$ls incoming-invoices
Invoice4711from-supplier-xyz.pdf
$ java -jar $(PATH_TO_JAR)/Mustang-CLI-2.4.1.jar --action extract --source incoming-invoices/ Invoice4711from-supplier-xyz.pdf --out factur-x.xml
$ls factur-x.xml
factur-x.xml
After extracting the XML document, a semantic model can be found within the 4GL code and queried by a call to the API function:
Invoice.fromXML(
String filename,
boolean recomputeComputedFields)

Demo example

IMPORT util
IMPORT FGL greruntime
IMPORT JAVA java.time.LocalDate
IMPORT JAVA java.time.format.DateTimeFormatter
IMPORT JAVA java.util.Collection
IMPORT JAVA java.util.Iterator
IMPORT JAVA com.fourjs.report.facturx.Codes
IMPORT JAVA com.fourjs.report.facturx.Percentage
IMPORT JAVA com.fourjs.report.facturx.UnitPriceAmount
IMPORT JAVA com.fourjs.report.facturx.Quantity
IMPORT JAVA com.fourjs.report.facturx.Amount
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.Invoice
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.Identifier
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.InvoiceLine
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.InvoiceTotalVATAmount
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.VATBreakdown
IMPORT JAVA com.fourjs.report.facturx.profiles.en16931.SellerVATIdentifier



SCHEMA officestore

TYPE OrderType RECORD
    orders RECORD LIKE orders.*,
    account RECORD LIKE account.*,
    country RECORD LIKE country.*,
    lineitem RECORD LIKE lineitem.*,
    product RECORD LIKE product.*,
    category RECORD LIKE category.*,
    item RECORD LIKE item.*
END RECORD

CONSTANT
    companyname = "FourJ's",
    companypostcode = "67800",
    companycity = "Schiltigheim",
    companyadressline1 = "123 avenue V. Hugo",
    companycountry = "FR",
    companyvatnumber = "FR38440422236",
    paymentterms = "Payment seven days after invoice date"

MAIN
     DEFINE
        handler om.SaxDocumentHandler
        
    -- load the 4rp file
    IF NOT fgl_report_loadCurrentSettings("Invoice.4rp") THEN
        EXIT PROGRAM
    END IF
    -- change some parameters
    CALL fgl_report_selectDevice("PDF")
    -- use the report
    LET handler = fgl_report_commitCurrentSettings()
    DISPLAY "Running report from database"
    CALL runReportFromDatabase(5, handler)
END MAIN

FUNCTION runReportFromDatabase(orderid, handler)
    DEFINE
        orderline OrderType,
        handler om.SaxDocumentHandler,
        orderid INTEGER

    DATABASE officestore
    DECLARE c_order CURSOR FOR
        SELECT orders.*,
            account.*,
            country.*,
            lineitem.*,
            product.*,
            category.*,
            item.*
            FROM orders, account, lineitem, product, category, item, country
            WHERE orders.orderid = orderid
                AND orders.orderid = lineitem.orderid
                AND orders.userid = account.userid
                AND lineitem.itemid = item.itemid
                AND item.productid = product.productid
                AND product.catid = category.catid
                AND country.code = orders.billcountry
            ORDER BY lineitem.linenum

    START REPORT invoice TO XML HANDLER handler
    FOREACH c_order INTO orderline.*
        OUTPUT TO REPORT invoice(orderline.*,handler)
        IF fgl_report_getErrorStatus() THEN
            DISPLAY "FGL: STOPPING REPORT, msg=\"",
                fgl_report_getErrorString(),
                "\""
            EXIT FOREACH
        END IF
    END FOREACH
    FINISH REPORT invoice
    DISPLAY "Produced " || fgl_report_getTotalNumberOfPages() || " page(s)"
    CLOSE c_order
END FUNCTION

FUNCTION getVATRate(category) RETURNS FLOAT
    DEFINE
        category STRING
        -- for the demo we set arbitrary VAT rate
    CASE category
        WHEN  "FURNITURE"
            RETURN 10
        WHEN  "ART"
            RETURN 5.5
        WHEN  "TRAVELLING"
            RETURN 2.1
        OTHERWISE
            RETURN 20
    END CASE
END FUNCTION

REPORT invoice(orderline,handler)
    DEFINE
        orderline OrderType,
        handler om.SaxDocumentHandler,
        dateString STRING,
        lineamount  DECIMAL(10,2),
        totalamountwithoutVAT  DECIMAL(10,2),
        totalamountwithVAT  DECIMAL(10,2),
        invoiceInstance Invoice,
        lineInstance InvoiceLine,   
        VATBreakDownList Collection,
        VATBase DECIMAL(10,2),
        VATRate DECIMAL(4,2),
        VATDetail DECIMAL(10,2),
        VATBreakdownInstance VATBreakdown, 
        sellerVATIdentifierInstance SellerVATIdentifier,
        it Iterator

    FORMAT
        FIRST PAGE HEADER
            LET dateString = orderline.orders.orderdate USING "yyyy-mm-dd"
            LET invoiceInstance =
                Invoice.create(Identifier.valueOf(util.JSON.stringify(orderline.orders.orderid)), 
                    Codes.InvoiceTypeCode.No380, --Business Invoice
                    LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE),
                    Codes.Currency.EUR)

            CALL invoiceInstance.setSellerName(companyname)
                .setSellerPostCode(companypostcode)
                .setSellerAddressLine1(companyadressline1)
                .setSellerCity(companycity)
                .setSellerCountryCode(Codes.CountryCodeList.fromString(companycountry))
                .setPaymentTerms(paymentterms) -- this or the payment due date are needed if amount due for payment is positive
                .setBuyerName(trim(orderline.account.firstname)||" "||trim(orderline.account.lastname))
                .setBuyerPostCode(trim(orderline.account.zip))
                .setBuyerAddressLine1(trim(orderline.account.addr1))
                .setBuyerAddressLine2(trim(orderline.account.addr2))
                .setBuyerCity(trim(orderline.account.city))
                .setBuyerCountryCode(Codes.CountryCodeList.fromString(orderline.account.country))
            
            LET sellerVATIdentifierInstance = invoiceInstance.appendNewSellerVATIdentifier() -- this is needed  if any standard VAT is computed in the invoice           
            CALL sellerVATIdentifierInstance.setSellerVATIdentifierSchemeIdentifier(Codes.SchemeIdentifierAttribute.VA)
                .setSellerVATIdentifier(Identifier.fromString(companyvatnumber))

            -- French standard VAT
            LET VATBreakdownInstance = invoiceInstance.appendNewVATBreakdown(Codes.VATCategoryCode.S)
            CALL VATBreakdownInstance.setVATCategoryRate(Percentage.valueOf(20)) --it is mandatory for computation

            -- French reduced VAT
            LET VATBreakdownInstance = invoiceInstance.appendNewVATBreakdown(Codes.VATCategoryCode.S)
            CALL VATBreakdownInstance.setVATCategoryRate(Percentage.valueOf(10)) --it is mandatory for computation

            LET VATBreakdownInstance = invoiceInstance.appendNewVATBreakdown(Codes.VATCategoryCode.S)
            CALL VATBreakdownInstance.setVATCategoryRate(Percentage.valueOf(5.5)) --it is mandatory for computation

            LET VATBreakdownInstance = invoiceInstance.appendNewVATBreakdown(Codes.VATCategoryCode.S)
            CALL VATBreakdownInstance.setVATCategoryRate(Percentage.valueOf(2.1)) --it is mandatory for computation
             
            PRINT companyname,
                companyadressline1,
                companypostcode,
                companycity,
                companycountry,
                paymentterms

        ON EVERY ROW

            LET lineInstance = invoiceInstance.appendNewInvoiceLine(
                    Identifier.fromString(util.JSON.stringify(orderline.lineitem.linenum)),
                    trim(orderline.product.proddesc),
                    Codes.UnitOfMeasure.C62, -- Unit is per Item
                    Quantity.valueOf(orderline.lineitem.quantity),
                    Codes.VATCategoryCode.S) -- standard VAT
                    .setInvoicedItemVATRate(Percentage.valueOf(getVATRate(orderline.category.catid)))
                    .setItemGrossPrice(UnitPriceAmount.valueOf(orderline.lineitem.unitprice))
        
            DISPLAY "        EVERY ROW " || orderline.lineitem.linenum
            LET lineamount = " "||lineInstance.getInvoiceLineNetAmount().getDisplayValue() -- compute the line amount
            LET totalamountwithoutVAT = " "||invoiceInstance.getInvoiceTotalAmountWithoutVAT().getDisplayValue() -- compute the running totals
            LET totalamountwithVAT = " "||invoiceInstance.getInvoiceTotalAmountWithVAT().getDisplayValue()

            PRINT orderline.*, lineamount, totalamountwithoutVAT, totalamountwithVAT
            
        ON LAST ROW
            LET VATBreakDownList = invoiceInstance.getVATBreakdowns()
            LET it = VATBreakDownList.iterator()
            WHILE it.hasNext()
                LET VATBreakdownInstance = CAST(it.next() AS VATBreakdown)
                LET VATBase = " "||VATBreakdownInstance.getVATCategoryTaxableAmount().getDisplayValue()
                LET VATRate = " "||VATBreakdownInstance.getVATCategoryRate().getDisplayValue()
                LET VATDetail = " "||VATBreakdownInstance.getVATCategoryTaxAmount().getDisplayValue()
                PRINT VATBase,VATRate,VATDetail
            END WHILE
       
          
            CALL fgl_report_setPDFFacturXAttachment(handler,invoiceInstance.getXMLAsDataURL()) -- embed in PDF
 
END REPORT


FUNCTION trim(s) RETURNS STRING
    DEFINE s STRING    
    RETURN s CLIPPED     
END FUNCTION