5. Creating PDF invoices (Basic profile)

To create a ZUGFeRD invoice, we now have to combine what we've learned in chapter 2 "Creating PDF/A files with iText" with what we've learned in chapter 4 "Creating XML Invoices with iText".

Creating PDF from scratch

In this chapter, we'll discuss a single example: PdfInvoicesBasic. In this example we'll create a PDF document for every invoice stored in our database. The PDF documents will be ZUGFeRD invoices using the Basic profile.

Let's start with the different constants defined in this class:

  1. public static final String DEST = "results/zugferd/pdf/basic%05d.pdf";
  2. public static final String ICC = "resources/color/sRGB_CS_profile.icm";
  3. public static final String REGULAR = "resources/fonts/OpenSans-Regular.ttf";
  4. public static final String BOLD = "resources/fonts/OpenSans-Bold.ttf";
  5. public static final String NEWLINE = "\n";

We recognize the pattern for the destination files, the color profile (as discussed in chapter 2), two font files, and a string with a newline character.

The main method of this example is very straightforward:

  1. public static void main(String[] args)
  2. throws IOException, ParserConfigurationException, SQLException,
  3. SAXException, TransformerException, ParseException
  4. DataIncompleteException, InvalidCodeException {
  5. LicenseKey.loadLicenseFile(
  6. System.getenv("ITEXT7_LICENSEKEY")
  7. + "/itextkey-html2pdf_typography.xml");
  8. File file = new File(DEST);
  9. file.getParentFile().mkdirs();
  10. PdfInvoicesBasic app = new PdfInvoicesBasic();
  11. PojoFactory factory = PojoFactory.getInstance();
  12. List<Invoice> invoices = factory.getInvoices();
  13. for (Invoice invoice : invoices) {
  14. app.createPdf(invoice);
  15. }
  16. factory.close();
  17. }

In this method, we create an instance of the PdfInvoicesBasic class; we get a List of Invoice objects from the PojoFactory; for every invoice, we call the createPdf() method.

  1. public void createPdf(Invoice invoice)
  2. throws ParserConfigurationException, SAXException, TransformerException,
  3. IOException, ParseException, DataIncompleteException, InvalidCodeException {
  4.  
  5. String dest = String.format(DEST, invoice.getId());
  6.  
  7. // Create the XML
  8. InvoiceData invoiceData = new InvoiceData();
  9. IBasicProfile basic = invoiceData.createBasicProfileData(invoice);
  10. InvoiceDOM dom = new InvoiceDOM(basic);
  11.  
  12. // Create the ZUGFeRD document
  13. ZugferdDocument pdfDocument = new ZugferdDocument(
  14. new PdfWriter(dest), ZugferdConformanceLevel.ZUGFeRDBasic,
  15. new PdfOutputIntent("Custom", "", "http://www.color.org",
  16. "sRGB IEC61966-2.1", new FileInputStream(ICC)));
  17. pdfDocument.addFileAttachment(
  18. "ZUGFeRD invoice", dom.toXML(), "ZUGFeRD-invoice.xml",
  19. PdfName.ApplicationXml, new PdfDictionary(), PdfName.Alternative);
  20.  
  21. // Create the document
  22. Document document = new Document(pdfDocument);
  23. document.setFont(PdfFontFactory.createFont(REGULAR, true))
  24. .setFontSize(12);
  25. PdfFont bold = PdfFontFactory.createFont(BOLD, true);
  26.  
  27. // Add the header
  28. document.add(
  29. new Paragraph()
  30. .setTextAlignment(TextAlignment.RIGHT)
  31. .setMultipliedLeading(1)
  32. .add(new Text(String.format("%s %s\n", basic.getName(), basic.getId()))
  33. .setFont(bold).setFontSize(14))
  34. .add(convertDate(basic.getDateTime(), "MMM dd, yyyy")));
  35. // Add the seller and buyer address
  36. document.add(getAddressTable(basic, bold));
  37. // Add the line items
  38. document.add(getLineItemTable(invoice, bold));
  39. // Add the grand totals
  40. document.add(getTotalsTable(
  41. basic.getTaxBasisTotalAmount(), basic.getTaxTotalAmount(),
  42. basic.getGrandTotalAmount(), basic.getGrandTotalAmountCurrencyID(),
  43. basic.getTaxTypeCode(), basic.getTaxApplicablePercent(),
  44. basic.getTaxBasisAmount(), basic.getTaxCalculatedAmount(),
  45. basic.getTaxCalculatedAmountCurrencyID(), bold));
  46. // Add the payment info
  47. document.add(getPaymentInfo(basic.getPaymentReference(),
  48. basic.getPaymentMeansPayeeFinancialInstitutionBIC(),
  49. basic.getPaymentMeansPayeeAccountIBAN()));
  50.  
  51. document.close();
  52. }

This createPdf() method consists of different parts:

  • We construct a file name (line 5) and we use the InvoiceData class (line 8) that was discussed in chapter 4 to create an IBasicProfile instance (line 9). We use this IBasicProfile instance to create an InvoiceDOM object (line 8). InvoiceDOM is one of the classes available in the pdfInvoice add-on.

  • We construct a ZugferdDocument instance (line 13) and we set the conformance level to ZUGFeRDBasic for the basic profile (line 14). We also add the output intent (line 15-16). We add the XML invoice as an attachment (line 17-19).

  • Then we create the document (line 22), and we add the content: a header (line 28-34), the address information of the seller and the buyer (line 36), the line items (line 38), the grand total and the tax information (line 40-45), and the payment information (line 47-49).

We create the header paragraph in the createPdf() method (lines 28-34), but we're using helper methods for the other content. Let's take a closer look at those methods.

Adding the seller and buyer addresses

Creating a PDF from scratch using iText is easy, but not trivial. It's easy, because you can use many different high-level objects, but it's not trivial as you have to create a design by writing code. In chapter 7, we'll see an alternative way to create PDF invoices that doesn't require you to write much code.

Most of the data that needs to be rendered is available through the IBasicProfile interface. See for instance the getAddressTable() method:

  1. public Table getAddressTable(IBasicProfile basic, PdfFont bold) {
  2. Table table = new Table(new UnitValue[]{
  3. new UnitValue(UnitValue.PERCENT, 50),
  4. new UnitValue(UnitValue.PERCENT, 50)})
  5. .setWidthPercent(100);
  6. table.addCell(getPartyAddress("From:",
  7. basic.getSellerName(),
  8. basic.getSellerLineOne(),
  9. basic.getSellerLineTwo(),
  10. basic.getSellerCountryID(),
  11. basic.getSellerPostcode(),
  12. basic.getSellerCityName(),
  13. bold));
  14. table.addCell(getPartyAddress("To:",
  15. basic.getBuyerName(),
  16. basic.getBuyerLineOne(),
  17. basic.getBuyerLineTwo(),
  18. basic.getBuyerCountryID(),
  19. basic.getBuyerPostcode(),
  20. basic.getBuyerCityName(),
  21. bold));
  22. table.addCell(getPartyTax(basic.getSellerTaxRegistrationID(),
  23. basic.getSellerTaxRegistrationSchemeID(), bold));
  24. table.addCell(getPartyTax(basic.getBuyerTaxRegistrationID(),
  25. basic.getBuyerTaxRegistrationSchemeID(), bold));
  26. return table;
  27. }

We create a table with two columns, and we use convenience methods to create the Cell instances:

  1. public Cell getPartyAddress(String who, String name,
  2. String line1, String line2, String countryID,
  3. String postcode, String city, PdfFont bold) {
  4. Paragraph p = new Paragraph()
  5. .setMultipliedLeading(1.0f)
  6. .add(new Text(who).setFont(bold)).add(NEWLINE)
  7. .add(name).add(NEWLINE)
  8. .add(line1).add(NEWLINE)
  9. .add(line2).add(NEWLINE)
  10. .add(String.format("%s-%s %s", countryID, postcode, city));
  11. Cell cell = new Cell()
  12. .setBorder(Border.NO_BORDER)
  13. .add(p);
  14. return cell;
  15. }
  16. public Cell getPartyTax(String[] taxId, String[] taxSchema, PdfFont bold) {
  17. Paragraph p = new Paragraph()
  18. .setFontSize(10).setMultipliedLeading(1.0f)
  19. .add(new Text("Tax ID(s):").setFont(bold));
  20. if (taxId.length == 0) {
  21. p.add("\nNot applicable");
  22. }
  23. else {
  24. int n = taxId.length;
  25. for (int i = 0; i < n; i++) {
  26. p.add(NEWLINE)
  27. .add(String.format("%s: %s", taxSchema[i], taxId[i]));
  28. }
  29. }
  30. return new Cell().setBorder(Border.NO_BORDER).add(p);
  31. }

With these helper methods, we already have the "upper part" of the invoice as shown in Figure 5.1.

Figure 5.1: first part of the invoice
Figure 5.1: first part of the invoice

We'll also use a Table to render the invoice lines.

Adding invoice lines

The part with the invoice lines is a Table with six columns that is created like this:

  1. public Table getLineItemTable(Invoice invoice, PdfFont bold) {
  2. Table table = new Table(
  3. new UnitValue[]{
  4. new UnitValue(UnitValue.PERCENT, 43.75f),
  5. new UnitValue(UnitValue.PERCENT, 12.5f),
  6. new UnitValue(UnitValue.PERCENT, 6.25f),
  7. new UnitValue(UnitValue.PERCENT, 12.5f),
  8. new UnitValue(UnitValue.PERCENT, 12.5f),
  9. new UnitValue(UnitValue.PERCENT, 12.5f)})
  10. .setWidthPercent(100)
  11. .setMarginTop(10).setMarginBottom(10);
  12. table.addHeaderCell(createCell("Item:", bold));
  13. table.addHeaderCell(createCell("Price:", bold));
  14. table.addHeaderCell(createCell("Qty:", bold));
  15. table.addHeaderCell(createCell("Subtotal:", bold));
  16. table.addHeaderCell(createCell("VAT:", bold));
  17. table.addHeaderCell(createCell("Total:", bold));
  18. Product product;
  19. for (Item item : invoice.getItems()) {
  20. product = item.getProduct();
  21. table.addCell(createCell(product.getName()));
  22. table.addCell(createCell(
  23. InvoiceData.format2dec(InvoiceData.round(product.getPrice())))
  24. .setTextAlignment(TextAlignment.RIGHT));
  25. table.addCell(createCell(String.valueOf(item.getQuantity()))
  26. .setTextAlignment(TextAlignment.RIGHT));
  27. table.addCell(createCell(
  28. InvoiceData.format2dec(InvoiceData.round(item.getCost())))
  29. .setTextAlignment(TextAlignment.RIGHT));
  30. table.addCell(createCell(
  31. InvoiceData.format2dec(InvoiceData.round(product.getVat())))
  32. .setTextAlignment(TextAlignment.RIGHT));
  33. table.addCell(createCell(
  34. InvoiceData.format2dec(InvoiceData.round(
  35. item.getCost() + ((item.getCost() * product.getVat()) / 100))))
  36. .setTextAlignment(TextAlignment.RIGHT));
  37. }
  38. return table;
  39. }

Again we use some helper methods to create Cell objects:

  1. public Cell createCell(String text) {
  2. return new Cell().setPadding(0.8f)
  3. .add(new Paragraph(text)
  4. .setMultipliedLeading(1));
  5. }
  6. public Cell createCell(String text, PdfFont font) {
  7. return new Cell().setPadding(0.8f)
  8. .add(new Paragraph(text)
  9. .setFont(font).setMultipliedLeading(1));
  10. }

The result looks like figure 5.2:

Figure 5.2: rendering the line items of an invoice
Figure 5.2: rendering the line items of an invoice

We need yet another table for the tax totals and the grand total.

Adding the totals

We add a table with all the totals like this:

  1. public Table getTotalsTable(String tBase, String tTax, String tTotal,
  2. String tCurrency, String[] type, String[] percentage, String base[],
  3. String tax[], String currency[], PdfFont bold) {
  4. Table table = new Table(
  5. new UnitValue[]{
  6. new UnitValue(UnitValue.PERCENT, 8.33f),
  7. new UnitValue(UnitValue.PERCENT, 8.33f),
  8. new UnitValue(UnitValue.PERCENT, 25f),
  9. new UnitValue(UnitValue.PERCENT, 25f),
  10. new UnitValue(UnitValue.PERCENT, 25f),
  11. new UnitValue(UnitValue.PERCENT, 8.34f)})
  12. .setWidthPercent(100);
  13. table.addCell(createCell("TAX:", bold));
  14. table.addCell(createCell("%", bold)
  15. .setTextAlignment(TextAlignment.RIGHT));
  16. table.addCell(createCell("Base amount:", bold));
  17. table.addCell(createCell("Tax amount:", bold));
  18. table.addCell(createCell("Total:", bold));
  19. table.addCell(createCell("Curr.:", bold));
  20. int n = type.length;
  21. for (int i = 0; i < n; i++) {
  22. table.addCell(createCell(type[i])
  23. .setTextAlignment(TextAlignment.RIGHT));
  24. table.addCell(createCell(percentage[i])
  25. .setTextAlignment(TextAlignment.RIGHT));
  26. table.addCell(createCell(base[i])
  27. .setTextAlignment(TextAlignment.RIGHT));
  28. table.addCell(createCell(tax[i])
  29. .setTextAlignment(TextAlignment.RIGHT));
  30. double total = Double.parseDouble(base[i]) + Double.parseDouble(tax[i]);
  31. table.addCell(createCell(
  32. InvoiceData.format2dec(InvoiceData.round(total)))
  33. .setTextAlignment(TextAlignment.RIGHT));
  34. table.addCell(createCell(currency[i]));
  35. }
  36. table.addCell(new Cell(1, 2).setBorder(Border.NO_BORDER));
  37. table.addCell(createCell(tBase, bold)
  38. .setTextAlignment(TextAlignment.RIGHT));
  39. table.addCell(createCell(tTax, bold)
  40. .setTextAlignment(TextAlignment.RIGHT));
  41. table.addCell(createCell(tTotal, bold)
  42. .setTextAlignment(TextAlignment.RIGHT));
  43. table.addCell(createCell(tCurrency, bold));
  44. return table;
  45. }

The code is very similar to what we did for the line items table. The only thing that is out of the ordinary, is that we create a cell that spans two columns in line 24-25. Figure 5.3 shows the result.

Figure 5.3: rendering the totals
Figure 5.3: rendering the totals

We're almost there. There only one piece of content missing.

Adding the payment info

Adding the payment info is just a matter of creating a Paragraph:

  1. public Paragraph getPaymentInfo(String ref, String[] bic, String[] iban) {
  2. Paragraph p = new Paragraph(String.format(
  3. "Please wire the amount due to our bank account using "
  4. + " the following reference: %s",
  5. ref));
  6. int n = bic.length;
  7. for (int i = 0; i < n; i++) {
  8. p.add(NEWLINE).add(String.format("BIC: %s - IBAN: %s", bic[i], iban[i]));
  9. }
  10. return p;
  11. }

Note that we made some assumptions about the payment means. ZUGFeRD allows for different payment types, but we assumed that payments have to be done by bank wire. The result is shown in figure 5.4:

Figure 5.4: payment info
Figure 5.4: payment info

In the createPdf() method, we combined all this content with a PDF invoice as result.

The final result

The goal of this example was to create a proof of concept, and Figure 5.5 shows the final result.

Figure 5.5: the resulting invoice
Figure 5.5: the resulting invoice

People who want to process the invoice manually, can do so; if they don't open the attachment panel, they won't even notice that there's an XML attachment inside. People who want to process the invoice automatically, can extract the XML or have their software extract the XML for processing.

I am aware that this invoice doesn't look very sexy. We could create tables with rounded borders, introduce a logo and some colors, we could add an extra sheet with terms-of-use, and so on, but that would lead us too far. In chapter 7, we'll discover that there is an alternative way to create PDF invoices, but let's take a look at some HTML first.