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. The main method of this example is very straightforward:

  1. PdfInvoicesBasic app = new PdfInvoicesBasic();
  2. PojoFactory factory = PojoFactory.getInstance();
  3. List<Invoice> invoices = factory.getInvoices();
  4. for (Invoice invoice : invoices) {
  5. app.createPdf(invoice);
  6. }
  7. factory.close();

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.

In the constructor, we initialize some variables:

  1. public static final String FONT = "resources/fonts/OpenSans-Regular.ttf";
  2. public static final String FONTB = "resources/fonts/OpenSans-Bold.ttf";
  3. protected Font font10;
  4. protected Font font10b;
  5. protected Font font12;
  6. protected Font font12b;
  7. protected Font font14;
  8. public PdfInvoicesBasic() throws DocumentException, IOException {
  9. BaseFont bf =
  10. BaseFont.createFont(FONT, BaseFont.WINANSI, BaseFont.EMBEDDED);
  11. BaseFont bfb =
  12. BaseFont.createFont(FONTB, BaseFont.WINANSI, BaseFont.EMBEDDED);
  13. font10 = new Font(bf, 10);
  14. font10b = new Font(bfb, 10);
  15. font12 = new Font(bf, 12);
  16. font12b = new Font(bfb, 12);
  17. font14 = new Font(bf, 14);
  18. }

FONT and FONTB contain the paths to the fonts we are going to embed. We use these paths when to create BaseFont objects. With these BaseFont objects, we create several Font objects with different font sizes.

In the createPdf() method, we create a file name and we use the InvoiceData class that was discussed in chapter 4 to create a BasicProfile instance.

  1. public void createPdf(Invoice invoice)
  2. throws ParserConfigurationException, SAXException, TransformerException,
  3. IOException, DocumentException, XMPException, ParseException,
  4. DataIncompleteException, InvalidCodeException {
  5. String dest = String.format(DEST, invoice.getId());
  6. InvoiceData invoiceData = new InvoiceData();
  7. BasicProfile basic = invoiceData.createBasicProfileData(invoice);
  8. // steps 1
  9. Document document = new Document();
  10. // step 2
  11. PdfAWriter writer = PdfAWriter.getInstance(document,
  12. new FileOutputStream(dest), PdfAConformanceLevel.ZUGFeRDBasic);
  13. writer.setPdfVersion(PdfWriter.VERSION_1_7);
  14. writer.createXmpMetadata();
  15. writer.getXmpWriter().setProperty(PdfAXmpWriter.zugferdSchemaNS,
  16. PdfAXmpWriter.zugferdDocumentFileName, "ZUGFeRD-invoice.xml");
  17. // step 3
  18. document.open();
  19. // step 4
  20. ICC_Profile icc = ICC_Profile.getInstance(new FileInputStream(ICC));
  21. writer.setOutputIntents(
  22. "Custom", "", "http://www.color.org", "sRGB IEC61966-2.1", icc);
  23. ...
  24. // step 5
  25. document.close();
  26. }

Steps 1, 2, 3, and 5 shouldn't have any secrets for us. They are explained in chapter 2, but let's take a look at step 4 in more detail.

Adding a title, date, addresses and tax info

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 6, 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 BasicProfile interface. For instance: we create the header for the invoice by asking the interface for the name, the id and the date of the document:

  1. Paragraph p;
  2. p = new Paragraph(basic.getName() + " " + basic.getId(), font14);
  3. p.setAlignment(Element.ALIGN_RIGHT);
  4. document.add(p);
  5. p = new Paragraph(convertDate(basic.getDateTime(), "MMM dd, yyyy"), font12);
  6. p.setAlignment(Element.ALIGN_RIGHT);
  7. document.add(p);

We pick a font with size 14 (as defined in the constructor) for the document name and a font with size 12 for the date. We align this information to the right. The date is converted to a String using the convertDate() method:

  1. public String convertDate(Date d, String newFormat) throws ParseException {
  2. SimpleDateFormat sdf = new SimpleDateFormat(newFormat);
  3. return sdf.format(d);
  4. }

We put the address and the tax info of the selling and the buying party next to each other by using a PdfPTable with two columns.

  1. PdfPTable table = new PdfPTable(2);
  2. table.setWidthPercentage(100);
  3. PdfPCell seller = getPartyAddress("From:",
  4. basic.getSellerName(),
  5. basic.getSellerLineOne(),
  6. basic.getSellerLineTwo(),
  7. basic.getSellerCountryID(),
  8. basic.getSellerPostcode(),
  9. basic.getSellerCityName());
  10. table.addCell(seller);
  11. PdfPCell buyer = getPartyAddress("To:",
  12. basic.getBuyerName(),
  13. basic.getBuyerLineOne(),
  14. basic.getBuyerLineTwo(),
  15. basic.getBuyerCountryID(),
  16. basic.getBuyerPostcode(),
  17. basic.getBuyerCityName());
  18. table.addCell(buyer);
  19. seller = getPartyTax(basic.getSellerTaxRegistrationID(),
  20. basic.getSellerTaxRegistrationSchemeID());
  21. table.addCell(seller);
  22. buyer = getPartyTax(basic.getBuyerTaxRegistrationID(),
  23. basic.getBuyerTaxRegistrationSchemeID());
  24. table.addCell(buyer);
  25. document.add(table);

We use convenience methods to create the PdfPCell instances:

  1. public PdfPCell getPartyAddress(String who, String name,
  2. String line1, String line2, String countryID, String postcode, String city) {
  3. PdfPCell cell = new PdfPCell();
  4. cell.setBorder(PdfPCell.NO_BORDER);
  5. cell.addElement(new Paragraph(who, font12b));
  6. cell.addElement(new Paragraph(name, font12));
  7. cell.addElement(new Paragraph(line1, font12));
  8. cell.addElement(new Paragraph(line2, font12));
  9. cell.addElement(new Paragraph(
  10. String.format("%s-%s %s", countryID, postcode, city), font12));
  11. return cell;
  12. }
  13. public PdfPCell getPartyTax(String[] taxId, String[] taxSchema) {
  14. PdfPCell cell = new PdfPCell();
  15. cell.setBorder(PdfPCell.NO_BORDER);
  16. cell.addElement(new Paragraph("Tax ID(s):", font10b));
  17. if (taxId.length == 0) {
  18. cell.addElement(new Paragraph("Not applicable", font10));
  19. }
  20. else {
  21. int n = taxId.length;
  22. for (int i = 0; i < n; i++) {
  23. cell.addElement(new Paragraph(
  24. String.format("%s: %s", taxSchema[i], taxId[i]), font10));
  25. }
  26. }
  27. return cell;
  28. }

In each of these methods, we create a PdfPCell, we remove the border, and we add a sequence of Paragraph objects. The result so far is 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 PdfPTable to render the invoice lines.

Adding invoice lines

Let's create a PdfPTable with six columns and define the column titles:

  1. table = new PdfPTable(6);
  2. table.setWidthPercentage(100);
  3. table.setSpacingBefore(10);
  4. table.setSpacingAfter(10);
  5. table.setWidths(new int[]{7, 2, 1, 2, 2, 2});
  6. table.addCell(getCell("Item:", Element.ALIGN_LEFT, font12b));
  7. table.addCell(getCell("Price:", Element.ALIGN_LEFT, font12b));
  8. table.addCell(getCell("Qty:", Element.ALIGN_LEFT, font12b));
  9. table.addCell(getCell("Subtotal:", Element.ALIGN_LEFT, font12b));
  10. table.addCell(getCell("VAT:", Element.ALIGN_LEFT, font12b));
  11. table.addCell(getCell("Total:", Element.ALIGN_LEFT, font12b));

In line 2, we tell iText that the table should take 100% of the available width (the default is 80%). The available width is determined by the page size (by default, all pages have size A4) minus the left and right margin (each margin is half an inch by default). In line 3 and 4, we add an extra spacing before and after the table. Measurement in PDF are done in user units. By default, one inch corresponds with 72 user units.

In line 5, we set the relative widths of the six columns.The first column will be seven times as wide as the third column. All the other columns will be twice as wide as the third column. From line 6 to 11, we add the cells with the column titles. We wrote a getCell() method to avoid that our code is cluttered with repetitive code:

  1. public PdfPCell getCell(String value, int alignment, Font font) {
  2. PdfPCell cell = new PdfPCell();
  3. cell.setUseAscender(true);
  4. cell.setUseDescender(true);
  5. Paragraph p = new Paragraph(value, font);
  6. p.setAlignment(alignment);
  7. cell.addElement(p);
  8. return cell;
  9. }

The BasicProfile interface doesn't provide all the information we need to populate the table, so we'll have to ask the Invoice object for its Items and loop over them to populate the table:

  1. Product product;
  2. for (Item item : invoice.getItems()) {
  3. product = item.getProduct();
  4. table.addCell(getCell(
  5. product.getName(), Element.ALIGN_LEFT, font12));
  6. table.addCell(getCell(
  7. InvoiceData.format2dec(InvoiceData.round(product.getPrice())),
  8. Element.ALIGN_RIGHT, font12));
  9. table.addCell(getCell(
  10. String.valueOf(item.getQuantity()), Element.ALIGN_RIGHT, font12));
  11. table.addCell(getCell(
  12. InvoiceData.format2dec(InvoiceData.round(item.getCost())),
  13. Element.ALIGN_RIGHT, font12));
  14. table.addCell(getCell(
  15. InvoiceData.format2dec(InvoiceData.round(product.getVat())),
  16. Element.ALIGN_RIGHT, font12));
  17. table.addCell(getCell(
  18. InvoiceData.format2dec(InvoiceData.round(
  19. item.getCost() + ((item.getCost() * product.getVat()) / 100))),
  20. Element.ALIGN_RIGHT, font12));
  21. }
  22. document.add(table);

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

Now let's create another table with the tax totals and the grand total.

Adding the totals

We add a table with all the totals like this:

  1. document.add(getTotalsTable(
  2. basic.getTaxBasisTotalAmount(), basic.getTaxTotalAmount(),
  3. basic.getGrandTotalAmount(), basic.getGrandTotalAmountCurrencyID(),
  4. basic.getTaxTypeCode(), basic.getTaxApplicablePercent(),
  5. basic.getTaxBasisAmount(), basic.getTaxCalculatedAmount(),
  6. basic.getTaxCalculatedAmountCurrencyID()));

We use all this data retrieved from the BasicProfile implementation to construct a PdfPTable:

  1. public PdfPTable getTotalsTable(String tBase, String tTax, String tTotal,
  2. String tCurrency, String[] type, String[] percentage, String base[],
  3. String tax[], String currency[]) throws DocumentException {
  4. PdfPTable table = new PdfPTable(6);
  5. table.setWidthPercentage(100);
  6. table.setWidths(new int[]{1, 1, 3, 3, 3, 1});
  7. table.addCell(getCell("TAX", Element.ALIGN_LEFT, font12b));
  8. table.addCell(getCell("%", Element.ALIGN_RIGHT, font12b));
  9. table.addCell(getCell("Base amount:", Element.ALIGN_LEFT, font12b));
  10. table.addCell(getCell("Tax amount:", Element.ALIGN_LEFT, font12b));
  11. table.addCell(getCell("Total:", Element.ALIGN_LEFT, font12b));
  12. table.addCell(getCell("", Element.ALIGN_LEFT, font12b));
  13. int n = type.length;
  14. for (int i = 0; i < n; i++) {
  15. table.addCell(getCell(type[i], Element.ALIGN_RIGHT, font12));
  16. table.addCell(getCell(percentage[i], Element.ALIGN_RIGHT, font12));
  17. table.addCell(getCell(base[i], Element.ALIGN_RIGHT, font12));
  18. table.addCell(getCell(tax[i], Element.ALIGN_RIGHT, font12));
  19. double total = Double.parseDouble(base[i]) + Double.parseDouble(tax[i]);
  20. table.addCell(getCell(InvoiceData.format2dec(
  21. InvoiceData.round(total)), Element.ALIGN_RIGHT, font12));
  22. table.addCell(getCell(currency[i], Element.ALIGN_LEFT, font12));
  23. }
  24. PdfPCell cell = getCell("", Element.ALIGN_LEFT, font12b);
  25. cell.setColspan(2);
  26. cell.setBorder(PdfPCell.NO_BORDER);
  27. table.addCell(cell);
  28. table.addCell(getCell(tBase, Element.ALIGN_RIGHT, font12b));
  29. table.addCell(getCell(tTax, Element.ALIGN_RIGHT, font12b));
  30. table.addCell(getCell(tTotal, Element.ALIGN_RIGHT, font12b));
  31. table.addCell(getCell(tCurrency, Element.ALIGN_LEFT, font12b));
  32. return table;
  33. }

The code is very similar to what we did before. 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

Once more, we provide the data as stored in the BasicProfile implementation:

  1. document.add(getPaymentInfo(
  2. basic.getPaymentReference(),
  3. basic.getPaymentMeansPayeeFinancialInstitutionBIC(),
  4. basic.getPaymentMeansPayeeAccountIBAN()));

This time, we use this data to construct 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 "
  4. + "using the following reference: %s",
  5. ref), font12);
  6. int n = bic.length;
  7. for (int i = 0; i < n; i++) {
  8. p.add(Chunk.NEWLINE);
  9. p.add(String.format("BIC: %s - IBAN: %s", bic[i], iban[i]));
  10. }
  11. return p;
  12. }

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

Now that we have all the content for the PDF, we can create the XML and attach it to the document.

Attaching the XML

We're almost there. We create an InvoiceDOM object with the BasicProfile instance and we create a PdfDictionary with the current date. We use these objects to add the attachment:

  1. InvoiceDOM dom = new InvoiceDOM(basic);
  2. PdfDictionary parameters = new PdfDictionary();
  3. parameters.put(PdfName.MODDATE, new PdfDate());
  4. PdfFileSpecification fileSpec = writer.addFileAttachment(
  5. "ZUGFeRD invoice", dom.toXML(), null,
  6. "ZUGFeRD-invoice.xml", "application/xml",
  7. AFRelationshipValue.Alternative, parameters);
  8. PdfArray array = new PdfArray();
  9. array.add(fileSpec.getReference());
  10. writer.getExtraCatalog().put(PdfName.AF, array);

You are free to change the description "ZUGFeRD invoice" into something else, such as "ZUGFeRD facture" or "Facture ZUGFeRD", but the XML file has to be named "ZUGFeRD-invoice.xml", the MIME-TYPE has to be "application/xml" and the Associated Files (AF) relationship has to be Alternative. The file is stored as an embedded file attachment, but you have to define it as an associated file (this is a PDF/A-3 requirement).

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.