4. Creating XML Invoices with iText

In chapter 1, we discussed XML standards for invoicing and we explained that ZUGFeRD is based on the Cross Industry Invoice (CII) standard and the Message User Guides (MUG) from CEN. In this chapter, we'll create a series of such invoices using iText.

Interfaces for the Basic and Comfort profile

iText ships with two interfaces that can be found in the com.itextpdf.text.zugferd.profiles package (shipped with the PDF/A jar):

  • BasicProfile: contains 56 get() methods for you to implement.

  • ComfortProfile: extends the BasicProfile interface and adds 89 more methods for you to implement (144 in total).

These methods look like this:

  1. public String getSellerName();
  2. public String getSellerPostcode();
  3. public String getSellerLineOne();
  4. public String getSellerLineTwo();
  5. public String getSellerCityName();
  6. public String getSellerCountryID();
  7. public String[] getSellerTaxRegistrationID();
  8. public String[] getSellerTaxRegistrationSchemeID();

As we're dealing with XML and as XML consists of data stored as text, most of the methods expect that you return String values, or arrays of String values, even when the data consists of numbers. In cases where dates or Boolean values are involved, the method has a Date, a Date[], a boolean, a Boolean[], or a Boolean[][] as return type.

iText ships with an implementation of these interfaces: BasicProfileImp implements the BasicProfile interface; ComfortProfileImp extends BasicProfileImp and implements the ComfortProfule interface. These classes store all the data in boolean, Date, String, List<String>, List<String[]>, List<Date>, List<Boolean>, or List<Boolean[]> member-variables and provide set() methods to populate these variables. For instance:

  1. public void setSellerName(String sellerName) {
  2. this.sellerName = sellerName;
  3. }
  4. public void setSellerPostcode(String sellerPostcode) {
  5. this.sellerPostcode = sellerPostcode;
  6. }
  7. public void setSellerLineOne(String sellerLineOne) {
  8. this.sellerLineOne = sellerLineOne;
  9. }
  10. public void setSellerLineTwo(String sellerLineTwo) {
  11. this.sellerLineTwo = sellerLineTwo;
  12. }
  13. public void setSellerCityName(String sellerCityName) {
  14. this.sellerCityName = sellerCityName;
  15. }
  16. public void setSellerCountryID(String sellerCountryID) {
  17. this.sellerCountryID = sellerCountryID;
  18. }
  19. public void addSellerTaxRegistration(String schemeID, String taxId) {
  20. sellerTaxRegistrationSchemeID.add(schemeID);
  21. sellerTaxRegistrationID.add(taxId);
  22. }

The provided values are used to implement the getter methods defined in the interface:

  1. public String getSellerName() {
  2. return sellerName;
  3. }
  4. public String getSellerPostcode() {
  5. return sellerPostcode;
  6. }
  7. public String getSellerLineOne() {
  8. return sellerLineOne;
  9. }
  10. public String getSellerLineTwo() {
  11. return sellerLineTwo;
  12. }
  13. public String getSellerCityName() {
  14. return sellerCityName;
  15. }
  16. public String getSellerCountryID() {
  17. return sellerCountryID;
  18. }
  19. public String[] getSellerTaxRegistrationID() {
  20. return to1DArray(sellerTaxRegistrationID);
  21. }
  22. public String[] getSellerTaxRegistrationSchemeID() {
  23. return to1DArray(sellerTaxRegistrationSchemeID);
  24. }

Not all the getters need to be fully implemented. Some data is optional in the Basic and Comfort profile. For instance, it is perfectly OK to implement some of the methods like this:

  1. public Date getBillingStartDateTime() {
  2. return null;
  3. }
  4. public String getBillingStartDateTimeFormat() {
  5. return null;
  6. }
  7. public Date getBillingEndDateTime() {
  8. return null;
  9. }
  10. public String getBillingEndDateTimeFormat() {
  11. return null;
  12. }

How you implement these interfaces will largely depend on the CRM you're using. You'll have to query its database and use the results of that query to implement the methods of the interface corresponding with the profile you want to support.

Getting and setting the data

In this tutorial, we'll use BasicProfileImp and ComfortProfileImp (the BasicProfile and ComfortProfile implementations that ship with iText) to store the information from the database we've discussed in chapter 3. See the InvoiceData class that we'll use in the next handful of examples.

  1. public InvoiceData() {
  2. }
  3. public BasicProfile createBasicProfileData(Invoice invoice) {
  4. BasicProfileImp profileImp = new BasicProfileImp();
  5. importData(profileImp, invoice);
  6. importBasicData(profileImp, invoice);
  7. return profileImp;
  8. }
  9. public ComfortProfile createComfortProfileData(Invoice invoice) {
  10. ComfortProfileImp profileImp = new ComfortProfileImp();
  11. importData(profileImp, invoice);
  12. importComfortData(profileImp, invoice);
  13. return profileImp;
  14. }

We don't pass any data to the InvoiceData constructor. Instead, we pass our Invoice POJO to the createBasicProfileData() or the createComfortProfile() method to obtain a BasicProfile or ComfortProfile implementation.

Internally, the default BasicProfileImp or ComfortProfileImp implementations are used. These data containers are populated using the importData(), importBasicData() and importComfortData() methods.

Basic and Comfort profile

The importData() method deals with data that is relevant for both the Basic and the Comfort profile:

  1. public void importData(BasicProfileImp profileImp, Invoice invoice) {
  2. profileImp.setTest(true);
  3. profileImp.setId(String.format("I/%05d", invoice.getId()));
  4. profileImp.setName("INVOICE");
  5. profileImp.setTypeCode(DocumentTypeCode.COMMERCIAL_INVOICE);
  6. profileImp.setDate(invoice.getInvoiceDate(), DateFormatCode.YYYYMMDD);
  7. profileImp.setSellerName("Das Company");
  8. profileImp.setSellerLineOne("ZUG Business Center");
  9. profileImp.setSellerLineTwo("Highway 1");
  10. profileImp.setSellerPostcode("9000");
  11. profileImp.setSellerCityName("Ghent");
  12. profileImp.setSellerCountryID("BE");
  13. profileImp.addSellerTaxRegistration(
  14. TaxIDTypeCode.FISCAL_NUMBER, "201/113/40209");
  15. profileImp.addSellerTaxRegistration(TaxIDTypeCode.VAT, "BE123456789");
  16. Customer customer = invoice.getCustomer();
  17. profileImp.setBuyerName(String.format("%s, %s",
  18. customer.getLastName(), customer.getFirstName()));
  19. profileImp.setBuyerPostcode(customer.getPostalcode());
  20. profileImp.setBuyerLineOne(customer.getStreet());
  21. profileImp.setBuyerCityName(customer.getCity());
  22. profileImp.setBuyerCountryID(customer.getCountryId());
  23. profileImp.setPaymentReference(String.format("%09d", invoice.getId()));
  24. profileImp.setInvoiceCurrencyCode("EUR");
  25. }

As you can see, we add some of the data, such as the name of the seller, in a hard-coded way. Obviously, this should be avoided in a real-world implementation.

We also use some constants such as DocumentTypeCode.COMMERCIAL_INVOICE, DateFormatCode.YYYYMMDD, and TaxIDTypeCode.VAT. DocumentTypeCode, DateFormatCode, TaxIDTypeCode and many other code list classes can be found in the com.itextpdf.text.zugferd.checkers packages. They all implement the CodeValidation class. This abstract class contains a check() method that throws an InvalidCodeException if the wrong code is provided. We'll look at these checkers in more detail in a moment.

Basic profile

All data that is necessary for the Basic profile is also necessary for the Comfort profile, but due to some differences between Basic and Comfort, the implementation of these profiles requires different setters in iText.

  1. public void importBasicData(BasicProfileImp profileImp, Invoice invoice) {
  2. profileImp.addNote(new String[]{
  3. "This is a test invoice.\nNothing on this invoice is real."
  4. + "\nThis invoice is part of a tutorial."});
  5. profileImp.addPaymentMeans("", "", "BE 41 7360 0661 9710",
  6. "", "", "KREDBEBB", "", "KBC");
  7. profileImp.addPaymentMeans("", "", "BE 56 0015 4298 7888",
  8. "", "", "GEBABEBB", "", "BNP Paribas");
  9. Map<Double,Double> taxes = new TreeMap<Double, Double>();
  10. double tax;
  11. for (Item item : invoice.getItems()) {
  12. tax = item.getProduct().getVat();
  13. if (taxes.containsKey(tax)) {
  14. taxes.put(tax, taxes.get(tax) + item.getCost());
  15. }
  16. else {
  17. taxes.put(tax, item.getCost());
  18. }
  19. profileImp.addIncludedSupplyChainTradeLineItem(
  20. format4dec(item.getQuantity()), "C62", item.getProduct().getName());
  21. }
  22. double total, tA;
  23. double ltN = 0;
  24. double ttA = 0;
  25. double gtA = 0;
  26. for (Map.Entry<Double, Double> t : taxes.entrySet()) {
  27. tax = t.getKey();
  28. total = round(t.getValue());
  29. gtA += total;
  30. tA = round((100 * total) / (100 + tax));
  31. ttA += (total - tA);
  32. ltN += tA;
  33. profileImp.addApplicableTradeTax(format2dec(total - tA), "EUR",
  34. TaxTypeCode.VALUE_ADDED_TAX, format2dec(tA), "EUR", format2dec(tax));
  35. }
  36. profileImp.setMonetarySummation(format2dec(ltN), "EUR",
  37. format2dec(0), "EUR",
  38. format2dec(0), "EUR",
  39. format2dec(ltN), "EUR",
  40. format2dec(ttA), "EUR",
  41. format2dec(gtA), "EUR");
  42. }

This is typical for the Basic profile:

  • The Basic profile allows free text in the header, see line 28-30: "This is a test invoice. Nothing on this invoice is real. This invoice is part of a tutorial."

  • You don't need that much information regarding the payment means.

  • You don't need that much information regarding the line items (the invoice lines).

Note that we loop over the different Item objects to calculate the monetary summation:

  • the total amount of the line items (line 62),

  • the tax basis amount (line 65), which is total of the line items after taking into account the charge total amount (line 63) and the allowance total amount (line 64), which are 0 in this case,

  • the total tax amount (line 66), and

  • the grand total (line 67).

Note that we use some helper methods to format numbers and percentages:

  1. public static double round(double d) {
  2. d = d * 100;
  3. long tmp = Math.round(d);
  4. return (double) tmp / 100;
  5. }
  6. public static String format2dec(double d) {
  7. return String.format("%.2f", d);
  8. }
  9. public static String format4dec(double d) {
  10. return String.format("%.4f", d);
  11. }

In some cases, numbers need to be expressed using 2 decimals; in other cases, numbers need to be expressed using 4 decimals. Once you create the XML, iText will throw an InvalidCodeException if you pass a numeric value with the wrong number of decimals.

Comfort profile

When we look at the importComfortData() method, we see that much more data can be provided. For instance: the notes in the header need to consist of qualified text. This means that we have to describe what the note is about using a code. In this case, we used FreeTextSubjectCode.REGULATORY_INFORMATION (see line 5).

  1. public void importComfortData(ComfortProfileImp profileImp, Invoice invoice) {
  2. profileImp.addNote(new String[]{
  3. "This is a test invoice.\nNothing on this invoice is real."
  4. + "\nThis invoice is part of a tutorial."},
  5. FreeTextSubjectCode.REGULATORY_INFORMATION);
  6. profileImp.addPaymentMeans(
  7. PaymentMeansCode.PAYMENT_TO_BANK_ACCOUNT,
  8. new String[]{"This is the preferred bank account."},
  9. "", "",
  10. "", "",
  11. "BE 41 7360 0661 9710", "", "",
  12. "", "", "",
  13. "KREDBEBB", "", "KBC");
  14. profileImp.addPaymentMeans(
  15. PaymentMeansCode.PAYMENT_TO_BANK_ACCOUNT,
  16. new String[]{"Use this as an alternative account."},
  17. "", "",
  18. "", "",
  19. "BE 56 0015 4298 7888", "", "",
  20. "", "", "",
  21. "GEBABEBB", "", "BNP Paribas");
  22. Map<Double,Double> taxes = new TreeMap<Double, Double>();
  23. double tax;
  24. int counter = 0;
  25. for (Item item : invoice.getItems()) {
  26. counter++;
  27. tax = item.getProduct().getVat();
  28. if (taxes.containsKey(tax)) {
  29. taxes.put(tax, taxes.get(tax) + item.getCost());
  30. }
  31. else {
  32. taxes.put(tax, item.getCost());
  33. }
  34. profileImp.addIncludedSupplyChainTradeLineItem(
  35. String.valueOf(counter),
  36. null,
  37. format4dec(item.getProduct().getPrice()), "EUR", null, null,
  38. null, null, null, null,
  39. null, null, null, null,
  40. format4dec(item.getQuantity()), "C62",
  41. new String[]{TaxTypeCode.VALUE_ADDED_TAX},
  42. new String[1],
  43. new String[]{TaxCategoryCode.STANDARD_RATE},
  44. new String[]{format2dec(item.getProduct().getVat())},
  45. format2dec(item.getCost()), "EUR",
  46. null, null,
  47. String.valueOf(item.getProduct().getId()), null,
  48. item.getProduct().getName(), null
  49. );
  50. }
  51. double total, tA;
  52. double ltN = 0;
  53. double ttA = 0;
  54. double gtA = 0;
  55. for (Map.Entry<Double, Double> t : taxes.entrySet()) {
  56. tax = t.getKey();
  57. total = round(t.getValue());
  58. gtA += total;
  59. tA = round((100 * total) / (100 + tax));
  60. ttA += (total - tA);
  61. ltN += tA;
  62. profileImp.addApplicableTradeTax(
  63. format2dec(total - tA), "EUR", TaxTypeCode.VALUE_ADDED_TAX,
  64. null, format2dec(tA), "EUR",
  65. TaxCategoryCode.STANDARD_RATE, format2dec(tax));
  66. }
  67. profileImp.setMonetarySummation(format2dec(ltN), "EUR",
  68. format2dec(0), "EUR",
  69. format2dec(0), "EUR",
  70. format2dec(ltN), "EUR",
  71. format2dec(ttA), "EUR",
  72. format2dec(gtA), "EUR");
  73. }

When browsing the code, you see that we can leave certain values empty ("") or pass null as a value. This isn't always true. iText will throw a DataIncompleteException when you try to make an XML based on incomplete data. As explained earlier, an InvalidDataException is thrown when the wrong data is provided, although iText doesn't check all the codes you pass.

Validation of the data

Table 4.1 shows an overview of the checker classes that will be used once iText creates an XML file based on your implementation of the BasicProfile or the ComfortProfile interface:

Table 4.1: Checker classes for the Basic and Comfort profile
Checker classDescriptionProfile

NumberChecker

Can be used to check if a number is an integer or a decimal; if a decimal, checks if it has two or four decimals.

Basic and higher

CountryCode

Just checks if the code consists of two uppercase letters (no numbers). It doesn't check if the country code actually exists. That's your responsibility.

Basic and higher

CurrencyCode

Just checks if the code consists of three uppercase letters (no numbers). It doesn't check if the currency code actually exists. That's your responsibility.

Basic and higher

DateFormatCode

Contains the three acceptable date formats YYYYMMDD (code 102), YYYYMM (code 610) and YYYYWW (code 616). This class also allows you to convert a Date to a String when given a format, and vice-versa.

Basic and higher

DocumentTypeCode

Could be COMMERCIAL_INVOICE (code 380), DEBIT_NOTE_FINANCIAL_ADJUSTMENT (code 38) and SELF_BILLED_INVOICE (code 389). iText will check if you're using the right profile for the document type.

Basic and higher

LanguageCode

Just checks if the code consists of two lowercase letters (no numbers). It doesn't check if the language code actually exists. That's your responsibility.

Basic and higher

MeasurementUnitCode

Contains constants for every possible measurement unit and checks if a code that was provided is one of these values.

Basic and higher

TaxIDTypeCode

Contains the codes for two types of tax ids (VAT and FISCAL_NUMBER) and checks if the code that was provided is one of these values.

Basic and higher

TaxTypeCode

Contains the codes for three types of tax (VALUE_ADDED_TAX, INSURANCE_TAX and TAX_ON_REPLACEMENT_PART) and checks if the code that was provided is one of these values

Basic and higher

FreeTextSubjectCode

Contains constants for every possible free text subject (to make it qualified text) and checks if the code that was provided is one of these values.

Comfort and higher

GlobalIdentifierCode

Contains a handful of frequently used codes, but only checks if the value consists of four numeric values.

Comfort and higher

PaymentMeansCode

Contains constants for all the possible payment means and checks if a code that was provided is one of these values.

Comfort and higher

TaxCategoryCode

Contains the codes for different tax categories and checks if a code that was provided is one of these values.

Comfort and higher

Text also has checkers that are only important in the context of the Extended profile, but iText doesn't generate XMLs using the Extended profile. Companies that need the Extended profile are assumed to already use specialized EDI software that creates XML that complies with the requirements of the Extended profile.

Let's finish this chapter by creating a series of XML files that comply with the Comfort profile.

Creating an XML file with iText

Once you have an implementation of the BasicProfile or the ComfortProfile interface, you can use it to create an InvoiceDOM object. The actual XML is created as a byte[] when you use the toXML() method. This is shown in the XmlInvoicesComfort example:

  1. public static void main(String[] args)
  2. throws SQLException, ParserConfigurationException, SAXException, IOException,
  3. TransformerException, DataIncompleteException, InvalidCodeException {
  4. File file = new File(DEST);
  5. file.getParentFile().mkdirs();
  6. PojoFactory factory = PojoFactory.getInstance();
  7. List<Invoice> invoices = factory.getInvoices();
  8. InvoiceData invoiceData = new InvoiceData();
  9. BasicProfile comfort;
  10. InvoiceDOM dom;
  11. for (Invoice invoice : invoices) {
  12. comfort = invoiceData.createComfortProfileData(invoice);
  13. dom = new InvoiceDOM(comfort);
  14. byte[] xml = dom.toXML();
  15. FileOutputStream fos
  16. = new FileOutputStream(String.format(DEST, invoice.getId()));
  17. fos.write(xml);
  18. fos.flush();
  19. fos.close();
  20. }
  21. factory.close();
  22. }

Just like in the example from chapter 4, we use the PojoFactory to get a list of invoices and we loop over every Invoice object. We use the InvoiceData class that was already discussed to create a ComfortProfile. We pass this profile to the InvoiceDOM constructor as if it were a BasicProfile instance, but InvoiceDOM is smart enough to see that it's actually a ComfortProfile instance.

In this case, we want the XML as a file, so we write the byte[] to a FileOutputStream. Figures 4.1, 4.2 and 4.3 show what such an XML looks like.

We're halfway creating a ZUGFeRD invoice: we already have the XML, now we need to create the PDF. That's what chapter 5 is about.!