PDFlib Cookbook

cookbook

pdfa/facturx_invoice_pdfa3b

Create a PDF/A-3b Factur-X (=ZUGFeRD 2.1) invoice and attach a delivery receipt as additional explanatory document.

Download Java Code  Show Output  Show Input (Lieferschein.pdf) 

/*
 * Create a PDF/A-3b Factur-X (=ZUGFeRD 2.1) invoice and attach a
 * delivery receipt as additional explanatory document
 *
 * The XML invoice uses the Factur-X profile "EN 16931 (COMFORT)". This must
 * match the Factur-X profile that is stored in the XMP input file.
 *
 * Required software: PDFlib+PDI/PPS 10
 * (set run_sample_with_pdi=false below to run the sample code without PDI)
 * Required data: two fonts, stationery PDF, additional reference document as PDF
 */

package com.pdflib.cookbook.pdflib.pdfa;

import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Calendar;
import java.util.Locale;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.pdflib.pdflib;
import com.pdflib.PDFlibException;

/*
 * Enable these import statements for validating the generated XML against the
 * Factur-X XML schema (see comment in the code for details)
import java.io.File;
import javax.xml.XMLConstants;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
 */

public class facturx_invoice_pdfa3b {
    private static final String COMMENT = "Created by PDFlib Cookbook program "
        + "facturx_invoice_pdfa3b.java\n"
        + "http://www.pdflib.com/pdflib-cookbook/";

    final static boolean run_sample_with_pdi = true;

    final static double fontsize = 11;
    final static double fontsizesmall = 9;

    final static double x_table = 55;
    final static double tablewidth = 475;
    final static double headerheight = 4 * fontsize;

    final static double y_address = 682;
    final static double x_salesrep = 455;
    final static double y_invoice = 542;
    final static double y_invoice_p2 = 800;
    final static double footer_bottom = 40;
    final static double bottom = footer_bottom + 4 * fontsize;
    final static double imagesize = 90;

    final static String fontname = "NotoSerif-Regular";

    final static String basefontoptions = "fontname=" + fontname + " fontsize="
        + fontsize;

    /*
     * XML namespaces used in the invoice document.
     */
    final static String NAMESPACE_A =
        "urn:un:unece:uncefact:data:standard:QualifiedDataType:100";
    final static String NAMESPACE_QDT =
            "urn:un:unece:uncefact:data:standard:QualifiedDataType:10";
    final static String NAMESPACE_RAM =
            "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100";
    final static String NAMESPACE_RSM =
        "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100";
    final static String NAMESPACE_UDT =
        "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100";

    /**
     * For simplicity only Euro is used as the currency.
     */
    final private static String CURRENCY = "EUR";

    /**
     * The German VAT percentage.
     */
    final private static double VAT_PERCENT = 19;
    
    /**
     * Payment due date in days from now.
     */
    final private static int DUE_DATE_DAYS = 30;

    /**
     * Encapsulation of information for buyer and seller.
     */
    static class trade_party {
        String name;
        String person_name;
        String postcode;
        String street;
        String line_two;
        String city;
        String country_code;
        String country;
        String phone;
        String fax;
        String email;
        String url;
        String id;
        String vat_id;
        String director;
        String company_registration;
        String bank_name;
        String iban;

        trade_party(String name, String person_name, String postcode,
            String street, String line_two, String city, String country_code,
            String country, String phone, String fax, String email, String url,
            String id, String vat_id, String director,
            String company_registration, String bank_name, String iban) {
            this.name = name;
            this.person_name = person_name;
            this.postcode = postcode;
            this.street = street;
            this.line_two = line_two;
            this.city = city;
            this.country_code = country_code;
            this.country = country;
            this.phone = phone;
            this.fax = fax;
            this.email = email;
            this.url = url;
            this.id = id;
            this.vat_id = vat_id;
            this.director = director;
            this.company_registration = company_registration;
            this.bank_name = bank_name;
            this.iban = iban;
        }
    }

    /**
     * Information about the sender of the invoice.
     */
    static final trade_party seller = new trade_party("Kraxi GmbH", null,
        "12345", "Flugzeugallee 17", null, "Papierfeld", "DE", "Deutschland",
        "(0123) 4567", "(0123) 4568", "info@kraxi.com", "www.kraxi.com", null,
        "DE123456789", "GF Paul Kraxi", "M\u00FCnchen HRB 999999",
        "Postbank M\u00FCnchen", "IBAN DE28700100809999999999");

    /**
     * Information about the recipient of the invoice.
     */
    static final trade_party buyer = new trade_party(
        "Papierflieger-Vertriebs-GmbH", "Helga Musterfrau", "34567",
        "Rabattstr. 25", null, "Osterhausen", "DE", "Deutschland", null, null,
        null, null, "987-654", null, null, null, null, null);
    
    /**
     * Encapsulation of information about an additional referenced document
     */
    static class additional_referenced_document {
        // The following are used in the XML element <ram:AdditionalReferencedDocument>:
        String IssuerAssignedID;
        String URIID;
        String TypeCode;

        // The following are used to describe the PDF attachment:
        String FileName;
        String MimeType;
        String Description;
        
        additional_referenced_document(
            String IssuerAssignedID, String URIID, String TypeCode,
            String FileName, String MimeType, String Description) {

            this.IssuerAssignedID = IssuerAssignedID;
            this.URIID = URIID;
            this.TypeCode = TypeCode;
            
            this.FileName = FileName;
            this.MimeType = MimeType;
            this.Description = Description;
        }
    }
    
    /**
     * Information about the additional referenced document
     * Type code 916 means "Referenzpapier"/"Additional supporting document"
     */
    static final additional_referenced_document refdoc =
        new additional_referenced_document(
            "L 123 456-78", "#ef=Lieferschein.pdf", "916",
            "Lieferschein.pdf", "application/pdf", "Delivery receipt");

    /**
     * Place company stationery.
     * 
     * @throws Exception
     */
    static void create_stationery(pdflib p) throws Exception {
        String sender = seller.name + " &#x2022; " + seller.street
            + " &#x2022; " + seller.postcode + " " + seller.city + " &#x2022; "
            + seller.country;
        String stationeryfontname = "NotoSerif-Regular";

        double y_company_logo = 748;

        String senderfull = seller.street + "\n" + seller.postcode + " "
            + seller.city + "\n" + seller.country
            + "<nextline leading=50%><nextparagraph leading=120%>" + "Tel. "
            + seller.phone + "\n" + "Fax " + seller.fax
            + "<nextline leading=50%><nextparagraph leading=120%>"
            + seller.email + "\n" + seller.url;

        /*
         * Print company logo and sender address
         */

        if (run_sample_with_pdi) {
            int page, stationery;
            String stationeryfilename = "kraxi_logo2_de.pdf";

            stationery = p.open_pdi_document(stationeryfilename, "");
            page = p.open_pdi_page(stationery, 1, "");

            p.fit_pdi_page(page, 0, y_company_logo,
                "boxsize={595 85} position={65 center}");

            p.close_pdi_page(page);
            p.close_pdi_document(stationery);
        }

        String optlist = basefontoptions + " fontsize=" + fontsizesmall
            + " fontname=" + stationeryfontname + " charref=true";
        p.fit_textline(sender, x_table, y_address + fontsize, optlist);

        /*
         * Print full company contact details
         */
        optlist = basefontoptions + " fontname=" + stationeryfontname
            + " leading=125% fillcolor={rgb 0.35 0.36 0.37}";
        int tf = p.create_textflow(senderfull, optlist);

        optlist = "verticalalign=bottom";
        p.fit_textflow(tf, x_salesrep, y_address, x_salesrep + imagesize,
            y_address + 150, optlist);
        p.delete_textflow(tf);

        /*
         * Print mandatory information about company in footer.
         */
        String footer = seller.name + "\tSitz der Gesellschaft\t"
            + "USt-IdNr\t" + seller.bank_name + "\n" + seller.director + "\t"
            + seller.company_registration + "\t" + seller.vat_id + "\t"
            + seller.iban;

        optlist = "ruler={" + (tablewidth * 0.2) + " " + (tablewidth * 0.45)
            + " " + (tablewidth * 0.7) + "} tabalignment={left left left}"
            + " hortabmethod=ruler " + basefontoptions + " fontsize="
            + fontsizesmall + " fontname=" + stationeryfontname;
        tf = p.add_textflow(-1, footer, optlist);
        p.fit_textflow(tf, x_table, footer_bottom, x_table + tablewidth,
            footer_bottom + 4 * fontsizesmall, "");
        p.delete_textflow(tf);
    }

    /**
     * Print receiver address.
     * 
     * @throws PDFlibException
     */
    static void create_address(pdflib p) throws PDFlibException {
        String address = buyer.name + "\n" + buyer.person_name + "\n"
            + buyer.street + "\n" + buyer.postcode + " " + buyer.city + "\n"
            + buyer.country;

        String optlist = basefontoptions + " leading=120%";
        int tf = p.create_textflow(address, optlist);

        p.fit_textflow(tf, x_table, y_address, x_table + tablewidth / 2,
            y_address - 100, "");
        p.delete_textflow(tf);
    }

    /**
     * Print header and date.
     * 
     * @throws PDFlibException
     */
    static void create_table_header(pdflib p, Calendar now,
        String invoice_number, String customer_number, String order_number)
        throws PDFlibException {
        String invoiceheader = "Rechnungsnummer: " + invoice_number;

        String date = "Liefer- und Rechnungsdatum: "
            + DateFormat.getDateInstance(DateFormat.LONG, Locale.GERMAN)
                .format(now.getTime());

        String optlist = "ruler={" + tablewidth + "} tabalignment={right} "
            + "hortabmethod=ruler " + basefontoptions;

        String text = invoiceheader + "\t" + date + "\n" + "\tKundennummer: "
            + customer_number + "\n" + "\tIhre Auftragsnummer: " + order_number
            + "\n" + "\tBetr\u00E4ge in " + CURRENCY;

        int tf = p.add_textflow(-1, text, optlist);
        p.fit_textflow(tf, x_table, y_invoice, x_table + tablewidth, y_invoice
            + headerheight, "");
        p.delete_textflow(tf);
    }

    /**
     * Information about the articles referenced in the invoice.
     */
    static class articledata {
        articledata(String name, double price, int quantity) {
            this.name = name;
            this.price = price;
            this.quantity = quantity;
        }

        String name;
        double price;
        int quantity;
    }

    public static void main(String argv[]) {

        /* This is where font/image/PDF input files live. Adjust as necessary. */
        String searchpath = "../input";

        /* By default annotations are also imported. In some cases this
         * requires the Noto fonts for creating annotation appearance streams.
         * We therefore set the searchpath to also point to the font directory.
         */
        String fontpath = "../resource/font";

        String payment_terms =
            "Zahlbar innerhalb von " + DUE_DATE_DAYS + " Tagen netto auf unser Konto. "
            + "Bitte geben Sie dabei die Rechnungsnummer an. Skontoabz\u00FCge "
            + "werden nicht akzeptiert.";

        articledata[] data = { new articledata("Superdrachen", 20, 2),
            new articledata("Turbo Flyer", 40, 5),
            new articledata("Sturzflug-Geier", 180, 1),
            new articledata("Eisvogel", 50, 3),
            new articledata("Storch", 20, 10),
            new articledata("Adler", 75, 1),
            new articledata("Kostenlose Zugabe", 0, 1) };

        String[] headers = { "Pos.", "Artikelbeschreibung", "Menge", "Preis",
            "Betrag" };

        String[] alignments = { "right", "left", "right", "right", "right" };

        /* Get the current date */
        Calendar calendar = Calendar.getInstance();

        String outfile = "facturx_invoice_pdfa3b.pdf";
        String title = "Factur-X Invoice";
        String xmpfile = "Factur-X_extension_schema.xmp";
        String xml_invoice_file = "factur-x.xml";
        String invoice_number = calendar.get(Calendar.YEAR) + "-03";
        String customer_number = "987-654";
        String order_number = "ABC-123";
        String optlist;

        /*
         * Format the numbers for Germany. Note that the numbers inside
         * the XML invoice must be formatted as plain numbers, without
         * any thousands separator.
         */
        final NumberFormat priceFormatDE = NumberFormat
            .getInstance(Locale.GERMAN);
        priceFormatDE.setMaximumFractionDigits(2);
        priceFormatDE.setMinimumFractionDigits(2);

        pdflib p = null;
        int exitcode = 0;

        try {
            /*
             * Set up document builder for building the Factur-X XML invoice.
             */
            Document dom = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder().newDocument();

            Element root = xml_create_root(dom);

            root.appendChild(dom.createComment(COMMENT));

            Element document_context = xml_create_document_context(dom);
            root.appendChild(document_context);

            Element document_header = xml_create_exchanged_document(dom,
                invoice_number, calendar);
            root.appendChild(document_header);
            
            p = new pdflib();

            p.set_option("SearchPath={" + searchpath + "}");

            p.set_option("SearchPath={" + fontpath + "}");

            /*
             * This means we don't have to check error return values, but will
             * get an exception in case of runtime problems.
             */
            p.set_option("errorpolicy=exception");

            p.begin_document(outfile, "pdfa=PDF/A-3b metadata={filename={"
                + xmpfile + "}}");

            p.set_info("Creator", "PDFlib Cookbook");
            p.set_info("Title", title);

            /* Use sRGB output intent so that we can use RGB color */
            if (p.load_iccprofile("sRGB", "usage=outputintent") == -1){
                System.err.println("Error: " + p.get_errmsg() );
                System.err.println("See www.pdflib.com for output intent ICC profiles.");
                p.delete();
                System.exit(2);
            }

            Element supply_chain_trade_transaction = dom.createElementNS(
                NAMESPACE_RSM, "rsm:SupplyChainTradeTransaction");
            root.appendChild(supply_chain_trade_transaction);
            
            /*
             * Create and place table with article list
             */
            
            /* Header row */
            int row = 1;
            int tbl = -1;
            int col;

            for (col = 1; col <= headers.length; col++) {
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} margin=2";
                tbl = p
                    .add_table_cell(tbl, col, row, headers[col - 1], optlist);
            }
            row++;

            double total = 0;

            /* Data rows: one for each article */
            for (int i = 0; i < data.length; i++) {
                double sum = data[i].price * data[i].quantity;
                col = 1;

                /* column 1: Position */
                int item_nr = i + 1;
                String item = Integer.toString(item_nr);
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, item, optlist);

                /* column 2: Artikelbeschreibung */
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} colwidth=50% margin=2";
                tbl = p.add_table_cell(tbl, col++, row, data[i].name, optlist);

                /* column 3: Menge */
                String quantity = Integer.toString(data[i].quantity);
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, quantity, optlist);

                /* column 4: Preis */
                String price = priceFormatDE.format(data[i].price);
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, price, optlist);

                /* column 5: Betrag */
                String sum_string = priceFormatDE.format(sum);
                optlist = "fittextline={position={" + alignments[col - 1]
                    + " center} " + basefontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, sum_string, optlist);

                Element line_item =
                    xml_create_included_supply_chain_trade_line_item(
                        dom, item, data[i].name, quantity, new BigDecimal(
                        data[i].price).setScale(2, RoundingMode.HALF_UP),
                    new BigDecimal(sum).setScale(2, RoundingMode.HALF_UP));
                supply_chain_trade_transaction.appendChild(line_item);

                row++;
                
                total += data[i].price * data[i].quantity;
            }

            Element applicable_header_trade_agreement =
                xml_create_applicable_header_trade_agreement(
                    dom, seller, buyer, refdoc);
            supply_chain_trade_transaction
                .appendChild(applicable_header_trade_agreement);

            Element delivery =
                xml_create_applicable_header_trade_delivery(dom, calendar);
            supply_chain_trade_transaction.appendChild(delivery);
            
            BigDecimal bd_netto = new BigDecimal(total).setScale(2,
                RoundingMode.HALF_UP);
            BigDecimal bd_vat_pct = new BigDecimal(VAT_PERCENT / 100);
            BigDecimal bd_vat = bd_netto.multiply(bd_vat_pct).setScale(2,
                    RoundingMode.HALF_UP);
            BigDecimal bd_brutto = bd_netto.add(bd_vat).setScale(2,
                    RoundingMode.HALF_UP);
            
            Element applicable_header_trade_settlement =
                xml_create_applicable_header_trade_settlement(
                    dom, invoice_number,
                    bd_netto, bd_vat, calendar, DUE_DATE_DAYS);
            supply_chain_trade_transaction
                .appendChild(applicable_header_trade_settlement);

            /* Print totals in the rightmost column */
            optlist = "fittextline={position={right center} " + basefontoptions
                + "} colspan=2 margin=2";
            tbl = p.add_table_cell(tbl, headers.length - 2, row,
                "Rechnungssumme netto", optlist);
            optlist = "fittextline={position={"
                + alignments[headers.length - 1] + " center} "
                + basefontoptions + "} margin=2";
            tbl = p.add_table_cell(tbl, headers.length, row++,
                priceFormatDE.format(bd_netto), optlist);

            optlist = "fittextline={position={right center} " + basefontoptions
                + "} colspan=2 margin=2";
            tbl = p.add_table_cell(tbl, headers.length - 2, row,
                "zuz\u00FCglich 19% MwSt.", optlist);
            optlist = "fittextline={position={"
                + alignments[headers.length - 1] + " center} "
                + basefontoptions + "} margin=2";
            tbl = p.add_table_cell(tbl, headers.length, row++,
                priceFormatDE.format(bd_vat), optlist);

            optlist = "fittextline={position={right center} " + basefontoptions
                + "} colspan=2 margin=2";
            tbl = p.add_table_cell(tbl, headers.length - 2, row,
                "Rechnungssumme brutto", optlist);
            optlist = "fittextline={position={"
                + alignments[headers.length - 1] + " center} "
                + basefontoptions + "} margin=2";
            tbl = p.add_table_cell(tbl, headers.length, row++,
                priceFormatDE.format(bd_brutto), optlist);

            /*
             * Add the total amounts to the Factur-X
             * ApplicableSupplyChainTradeSettlement element.
             */
            Element monetary_summation = xml_create_monetary_summation(dom,
                bd_netto, bd_vat, bd_brutto);
            applicable_header_trade_settlement.appendChild(monetary_summation);

            /* Footer row with terms of payment */
            optlist = basefontoptions + " alignment=justify leading=120%";
            int tf = p.create_textflow(payment_terms, optlist);

            optlist = "rowheight=1 margin=2 margintop=" + (2 * fontsize)
                + " colspan=" + headers.length + " textflow=" + tf;
            tbl = p.add_table_cell(tbl, 1, row++, "", optlist);

            /*
             * Place the table instance(s), creating pages as required.
             */
            String result;
            int pagecount = 0;
            do {
                double top;

                p.begin_page_ext(0, 0, "width=a4.width height=a4.height");

                if (++pagecount == 1) {
                    create_stationery(p);
                    create_address(p);

                    top = y_invoice - 3 * fontsize;

                    create_table_header(p, calendar, invoice_number,
                        customer_number, order_number);
                }
                else {
                    top = y_invoice_p2;
                }
                /* Place the table on the page; Shade every other row. */
                optlist = "header=1 fill={{area=rowodd fillcolor={gray 0.9}}}";

                result = p.fit_table(tbl, x_table, bottom,
                    x_table + tablewidth, top, optlist);

                if (result.equals("_error")) {
                    throw new Exception("Couldn't place table: "
                        + p.get_errmsg());
                }

                p.end_page_ext("");
            }
            while (result.equals("_boxfull"));

            p.delete_table(tbl, "");

            DOMSource source = new DOMSource(root);

            /*
             * Optional code to validate the generated XML file against
             * the schema. This is useful for development purposes. Replace
             * "/your/path/to/the/schema/FACTUR-X_EN16931.xsd" with the actual
             * pathname to the Factur-X XML schema for validation according to
             * EN 16931, and activate the code by removing this comment.
             * Also enable the corresponding import statements at the top of
             * the file.
             *
            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            Schema schema = factory.newSchema(new File("/your/path/to/the/schema/FACTUR-X_EN16931.xsd"));
            
            Validator validator = schema.newValidator();
            validator.validate(source);
            */
            
            /*
             * Create XML document and put it into a PVF file.
             */
            StringWriter sw = new StringWriter();
            StreamResult xml_result = new StreamResult(sw);

            Transformer transformer = TransformerFactory.newInstance()
                .newTransformer();
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(
                "{http://xml.apache.org/xslt}indent-amount", "2");
            transformer.transform(source, xml_result);

            String pvf_name = "/pvf/" + xml_invoice_file;
            byte[] xml_bytes = sw.toString().getBytes("UTF-8");
            p.create_pvf(pvf_name, xml_bytes, "");

            /*
             * Load the XML file for the invoice and associate it with the
             * document with relationship "Alternative".
             * Both attachments are marked as "document attachments" to
             * make them visible in Acrobat's attachment pane.
             */
            int xml_invoice = p.load_asset("Attachment", pvf_name,
                "mimetype=text/xml " +
                "description={Factur-X invoice data in XML format} " +
                "relationship=Alternative documentattachment=true");

            /*
             * Load the explanatory document (delivery receipt) and associate
             * it with the document with relationship "Supplement".
             */
            int delivery_receipt = p.load_asset("Attachment", refdoc.FileName,
                "mimetype=" + refdoc.MimeType + " " +
                "description={" + refdoc.Description + "} " +
                "relationship=Supplement documentattachment=true");

            p.end_document("associatedfiles={" + xml_invoice + " " + delivery_receipt + "}");

            p.delete_pvf(pvf_name);
        }
        catch (PDFlibException e) {
            System.err.println("PDFlib exception occurred:");
            System.err.println("[" + e.get_errnum() + "] " + e.get_apiname() +
                ": " + e.get_errmsg());
            exitcode = 1;
        }
        catch (Exception e) {
            System.err.println(e);
            exitcode = 1;
        }
        finally {
            if (p != null) {
                p.delete();
            }
            System.exit(exitcode);
        }
    }

    /**
     * Create a "ram:IncludedSupplyChainTradeLineItem" XML element.
     * 
     * @param dom
     *            XML document
     * @param position
     *            Position in invoice
     * @param name
     *            Article name
     * @param quantity
     *            Quantity of article
     * @param price
     *            Price per article
     * @param sum
     *            Sum for position
     *            
     * @return XML element "ram:IncludedSupplyChainTradeLineItem"
     */
    private static Element xml_create_included_supply_chain_trade_line_item(
        Document dom, String position, String name, String quantity,
        BigDecimal price, BigDecimal sum) {
        Element line_item = dom
            .createElementNS(NAMESPACE_RAM, "ram:IncludedSupplyChainTradeLineItem");

        Element line_document = dom
            .createElementNS(NAMESPACE_RAM, "ram:AssociatedDocumentLineDocument");
        append_text_element(dom, line_document, NAMESPACE_RAM, "ram:LineID", position);
        line_item.appendChild(line_document);
        
        Element product = dom.createElementNS(NAMESPACE_RAM, "ram:SpecifiedTradeProduct");
        append_text_element(dom, product, NAMESPACE_RAM, "ram:Name", name);
        line_item.appendChild(product);   
        
        Element agreement = dom.createElementNS(NAMESPACE_RAM, "ram:SpecifiedLineTradeAgreement");
        Element trade_price = dom.createElementNS(NAMESPACE_RAM, "ram:NetPriceProductTradePrice");
        append_currency_element(dom, trade_price,
                NAMESPACE_RAM, "ram:ChargeAmount", price);
        agreement.appendChild(trade_price);
        line_item.appendChild(agreement);
        
        Element delivery = dom
            .createElementNS(NAMESPACE_RAM, "ram:SpecifiedLineTradeDelivery");
        Element billed_quantity = append_text_element(dom, delivery,
            NAMESPACE_RAM, "ram:BilledQuantity",
            new BigDecimal(quantity).setScale(4).toString());
        
        /*
         * Unit code C62 means "one item".
         */
        billed_quantity.setAttribute("unitCode", "C62");
        line_item.appendChild(delivery);

        Element trade_settlement = dom
            .createElementNS(NAMESPACE_RAM, "ram:SpecifiedLineTradeSettlement");
        Element trade_tax = dom
                .createElementNS(NAMESPACE_RAM, "ram:ApplicableTradeTax");
        append_text_element(dom, trade_tax, NAMESPACE_RAM, "ram:TypeCode", "VAT");
        append_text_element(dom, trade_tax, NAMESPACE_RAM, "ram:CategoryCode", "S");
        append_text_element(dom, trade_tax, NAMESPACE_RAM, "ram:RateApplicablePercent",
                new BigDecimal(VAT_PERCENT).setScale(2).toString());
        trade_settlement.appendChild(trade_tax);
        
        Element monetary_summation = dom
                .createElementNS(NAMESPACE_RAM, "ram:SpecifiedTradeSettlementLineMonetarySummation");
        append_currency_element(dom, monetary_summation,
                NAMESPACE_RAM, "ram:LineTotalAmount", sum);
        trade_settlement.appendChild(monetary_summation);
        
        line_item.appendChild(trade_settlement);

        return line_item;
    }

    /**
     * Create a "ram:SpecifiedTradeSettlementHeaderMonetarySummation" XML element.
     * 
     * @param dom
     *            XML document
     * @param netto
     *            Netto price
     * @param vat
     *            Calculated VAT sum
     * @param brutto
     *            Brutto price (netto + vat)
     * 
     * @return New "ram:SpecifiedTradeSettlementHeaderMonetarySummation" XML element
     */
    private static Element xml_create_monetary_summation(Document dom,
        BigDecimal netto, BigDecimal vat, BigDecimal brutto) {
        Element monetary_summation = dom
            .createElementNS(NAMESPACE_RAM, "ram:SpecifiedTradeSettlementHeaderMonetarySummation");

        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:LineTotalAmount", netto);
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:TaxBasisTotalAmount", netto);
        append_currency_element_with_id(dom, monetary_summation,
            NAMESPACE_RAM, "ram:TaxTotalAmount", vat);
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:GrandTotalAmount", brutto);
        append_currency_element(dom, monetary_summation,
                NAMESPACE_RAM, "ram:DuePayableAmount", brutto);

        return monetary_summation;
    }

    /**
     * Create "ram:ApplicableHeaderTradeSettlement" XML element.
     * 
     * @param dom
     *            XML document
     * @param invoice_number
     *            Invoice number
     * @param netto
     *            Net sum
     * @param vat
     *            Calculated VAT
     * @param now
     *            Current date
     * @param due_date_days
     *            Days until due date
     * 
     * @return New "ram:ApplicableHeaderTradeSettlement" XML element
     */
    private static Element xml_create_applicable_header_trade_settlement(
        Document dom, String invoice_number,
        BigDecimal netto, BigDecimal vat, Calendar now, int due_date_days) {
        Element settlement = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableHeaderTradeSettlement");

        append_text_element(dom, settlement,
            NAMESPACE_RAM, "ram:PaymentReference", invoice_number);
        append_text_element(dom, settlement,
            NAMESPACE_RAM, "ram:InvoiceCurrencyCode", CURRENCY);

        Element applicable_trade_tax = dom.createElementNS(NAMESPACE_RAM, "ram:ApplicableTradeTax");
        append_currency_element(dom, applicable_trade_tax,
            NAMESPACE_RAM, "ram:CalculatedAmount", vat);
        append_text_element(dom, applicable_trade_tax, NAMESPACE_RAM, "ram:TypeCode", "VAT");
        append_currency_element(dom, applicable_trade_tax,
            NAMESPACE_RAM, "ram:BasisAmount", netto);
        append_text_element(dom, applicable_trade_tax, NAMESPACE_RAM, "ram:CategoryCode", "S");
        append_text_element(dom, applicable_trade_tax, NAMESPACE_RAM, "ram:RateApplicablePercent",
            new BigDecimal(VAT_PERCENT).setScale(2).toString());
        settlement.appendChild(applicable_trade_tax);

        Element payment_terms = dom
                .createElementNS(NAMESPACE_RAM, "ram:SpecifiedTradePaymentTerms");
        Calendar due_date = (Calendar) now.clone();
        due_date.add(Calendar.DATE, due_date_days);
        append_date_time_string_element(dom,
                payment_terms, NAMESPACE_RAM, "ram:DueDateDateTime", due_date);
        settlement.appendChild(payment_terms);
        
        return settlement;
    }

    /**
     * Create "rsm:ExchangedDocumentContext" XML element.
     * 
     * @param dom
     *            XML document
     * 
     * @return New "rsm:ExchangedDocumentContext" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_document_context(Document dom)
        throws DOMException {
        Element document_context = dom.createElementNS(NAMESPACE_RSM,
            "rsm:ExchangedDocumentContext");
        
        Element context_param = dom
            .createElementNS(NAMESPACE_RAM,
                "ram:GuidelineSpecifiedDocumentContextParameter");
        document_context.appendChild(context_param);

        // Identifier for EN 16931 (COMFORT) profile:
        append_text_element(dom, context_param, NAMESPACE_RAM, "ram:ID",
            "urn:cen.eu:en16931:2017");

        return document_context;
    }

    /**
     * Create "rsm:ExchangedDocument" XML element.
     * 
     * @param dom
     *            XML document
     * @param invoice_number
     *            Invoice number
     * @param now
     *            Timestamp for invoice creation
     * 
     * @return New "rsm:ExchangedDocument" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_exchanged_document(Document dom,
        String invoice_number, Calendar now)
        throws DOMException {

        Element header = dom.createElementNS(NAMESPACE_RSM,
            "rsm:ExchangedDocument");

        append_text_element(dom, header, NAMESPACE_RAM, "ram:ID", invoice_number);

        /*
         * Type code 380: "Handelsrechnung"/"Commercial invoice"
         */
        append_text_element(dom, header, NAMESPACE_RAM, "ram:TypeCode", "380");

        append_date_time_string_element(dom,
            header, NAMESPACE_RAM, "ram:IssueDateTime", now);
        
        Element note_element = dom.createElementNS(NAMESPACE_RAM,
            "ram:IncludedNote");
        append_text_element(dom, note_element, NAMESPACE_RAM, "ram:Content",
            COMMENT);
        header.appendChild(note_element);
        
        return header;
    }

    /**
     * Create "udt:DateTimeString" XML element.
     * 
     * @param dom
     *            XML document
     * @param parent
     *            Parent XML element for new XML element
     * @param namespace_uri
     *            Namespace URI of new XML element
     * @param element_name
     *            Name of new XML element
     * @param now
     *            Timestamp
     *            
     * @throws DOMException
     */
    private static void append_date_time_string_element(Document dom,
            Element parent,
            String namespace_uri, String element_name, Calendar now)
                throws DOMException {
        String date_string = "" + now.get(Calendar.YEAR)
            + String.format("%02d", now.get(Calendar.MONTH) + 1)
            + String.format("%02d", now.get(Calendar.DAY_OF_MONTH));
        
        Element date_time = dom.createElementNS(namespace_uri,
                                                        element_name);
        Element date_time_string = append_text_element(dom, date_time,
            NAMESPACE_UDT, "udt:DateTimeString", date_string);
        date_time_string.setAttribute("format", "102");
        date_time.appendChild(date_time_string);
        
        parent.appendChild(date_time);
    }

    /**
     * Convenience function for inserting an element with a namespace prefix
     * with text contents into a parent XML element.
     * 
     * @param dom
     *            XML document
     * @param parent
     *            Parent XML element for new XML element
     * @param namespace_uri
     *            Namespace URI of new XML element
     * @param element_name
     *            Name of new XML element
     * @param value
     *            Contents for new XML element
     * 
     * @return The new XML element
     * 
     * @throws DOMException
     */
    private static Element append_text_element(Document dom, Element parent,
        String namespace_uri, String element_name, String value)
                                                    throws DOMException {
        Element new_element = dom.createElementNS(namespace_uri, element_name);
        
        new_element.appendChild(dom.createTextNode(value));
        parent.appendChild(new_element);
        
        return new_element;
    }
    
    /**
     * Convenience function for inserting an element that is a currency value
     * into a parent XML element. The currency identifier is fixed.
     * 
     * @param dom
     *            XML document
     * @param parent
     *            Parent XML element for new XML element
     * @param namespace_uri
     *            Namespace uri of new XML element
     * @param element_name
     *            Name of new XML element
     * @param value
     *            Value for the new XML element
     * 
     * @return The new XML element
     * 
     * @throws DOMException
     */
    private static Element append_currency_element_with_id(Document dom,
            Element parent, String namespace_uri, String element_name,
            BigDecimal value)
                    throws DOMException {
        /*
         * Format the number as a plain number, without any thousands
         * separator, with two decimals after the decimal separator.
         */
        Element new_element = append_currency_element(dom, parent, namespace_uri,
                                element_name, value);
        new_element.setAttribute("currencyID", CURRENCY);
        
        return new_element;
    }

    /**
     * Convenience function for inserting an element that is a currency value
     * into a parent XML element, without "currencyID" attribute
     * 
     * @param dom
     *            XML document
     * @param parent
     *            Parent XML element for new XML element
     * @param namespace_uri
     *            Namespace uri of new XML element
     * @param element_name
     *            Name of new XML element
     * @param value
     *            Value for the new XML element
     * 
     * @return The new XML element
     * 
     * @throws DOMException
     */
    private static Element append_currency_element(Document dom,
        Element parent, String namespace_uri, String element_name,
        BigDecimal value)
        throws DOMException {
        /*
         * Format the number as a plain number, without any thousands
         * separator, with two decimals after the decimal separator.
         */
        Element new_element = append_text_element(dom, parent, namespace_uri,
            element_name, value.setScale(2).toPlainString());

        return new_element;
    }

    /**
     * Create the root element of the XML invoice.
     * 
     * @param dom
     *            XML document
     * 
     * @return The new root XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_root(Document dom) throws DOMException {
        /*
         * Create root node of invoice XML with the necessary attributes for
         * declaring namespaces and Factur-X XML schema.
         */
        Element root = dom.createElementNS(NAMESPACE_RSM, "rsm:CrossIndustryInvoice");
        
        root.setAttribute("xmlns:xsi",
            "http://www.w3.org/2001/XMLSchema-instance");
        root.setAttribute("xmlns:a", NAMESPACE_A);
        root.setAttribute("xmlns:qdt", NAMESPACE_QDT);
        root.setAttribute("xmlns:ram", NAMESPACE_RAM);
        root.setAttribute("xmlns:rsm", NAMESPACE_RSM);
        root.setAttribute("xmlns:udt", NAMESPACE_UDT);
        
        return root;
    }

    /**
     * Create a trade party element.
     * 
     * @param dom
     *            XML document
     * @param namespace_uri
     *            Namespace for the new XML element
     * @param element_name
     *            Name for the new XML element
     * @param party
     *            Information for creating the element
     * 
     * @return The new XML element
     */
    private static Element xml_create_trade_party(Document dom,
        String namespace_uri, String element_name, trade_party party) {
        Element trade_party_element =
            dom.createElementNS(namespace_uri, element_name);

        append_text_element(dom, trade_party_element,
            NAMESPACE_RAM, "ram:Name", party.name);

        Element postal_trade_address =
            dom.createElementNS(NAMESPACE_RAM, "ram:PostalTradeAddress");
        trade_party_element.appendChild(postal_trade_address);

        append_text_element(dom, postal_trade_address,
            NAMESPACE_RAM, "ram:PostcodeCode", party.postcode);
        append_text_element(dom, postal_trade_address,
            NAMESPACE_RAM, "ram:LineOne", party.street);
        if (party.line_two != null) {
            append_text_element(dom, postal_trade_address,
                NAMESPACE_RAM, "ram:LineTwo", party.line_two);
        }
        append_text_element(dom, postal_trade_address,
            NAMESPACE_RAM, "ram:CityName", party.city);
        append_text_element(dom, postal_trade_address,
            NAMESPACE_RAM, "ram:CountryID", party.country_code);

        if (party.vat_id != null) {
            Element tax_registration = dom
                .createElementNS(NAMESPACE_RAM, "ram:SpecifiedTaxRegistration");
            Element id = append_text_element(dom, tax_registration,
                NAMESPACE_RAM, "ram:ID", party.vat_id);
            id.setAttribute("schemeID", "VA");
            trade_party_element.appendChild(tax_registration);
        }

        return trade_party_element;
    }

    /**
     * Create an AdditionalReferencedDocument element.
     * 
     * @param dom
     *            XML document
     * @param namespace_uri
     *            Namespace for the new XML element
     * @param element_name
     *            Name for the new XML element
     * @param refdoc
     *            Information for creating the element
     * 
     * @return The new XML element
     */
    private static Element xml_create_additional_referenced_document(Document dom,
        String namespace_uri, String element_name, additional_referenced_document refdoc) {
        Element referenced_document =
            dom.createElementNS(namespace_uri, element_name);

        append_text_element(dom, referenced_document,
            NAMESPACE_RAM, "ram:IssuerAssignedID", refdoc.IssuerAssignedID);

        append_text_element(dom, referenced_document,
            NAMESPACE_RAM, "ram:URIID", refdoc.URIID);

        append_text_element(dom, referenced_document,
            NAMESPACE_RAM, "ram:TypeCode", refdoc.TypeCode);
        
        return referenced_document;
    }

    /**
     * Create an "ram:ApplicableHeaderTradeAgreement" XML element.
     * 
     * @param dom
     *            XML document
     * @param seller
     *            Seller information
     * @param buyer
     *            Buyer information
     * 
     * @return The new "ram:ApplicableHeaderTradeAgreement" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_applicable_header_trade_agreement(
        Document dom, trade_party seller, trade_party buyer,
        additional_referenced_document refdoc)
        throws DOMException {
        Element agreement = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableHeaderTradeAgreement");

        Element seller_trade_party = xml_create_trade_party(dom,
            NAMESPACE_RAM, "ram:SellerTradeParty", seller);
        agreement.appendChild(seller_trade_party);

        Element buyer_trade_party = xml_create_trade_party(dom,
            NAMESPACE_RAM, "ram:BuyerTradeParty", buyer);
        agreement.appendChild(buyer_trade_party);

        Element additional_referenced_document = xml_create_additional_referenced_document(dom,
            NAMESPACE_RAM, "ram:AdditionalReferencedDocument", refdoc);
        agreement.appendChild(additional_referenced_document);

        return agreement;
    }
    
    /**
     * Create an "ram:ApplicableHeaderTradeDelivery" XML element.
     * 
     * @param dom
     *            XML document
     * @param seller
     *            Seller information
     * @param buyer
     *            Buyer information
     * 
     * @return The new "ram:ApplicableHeaderTradeDelivery" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_applicable_header_trade_delivery(
        Document dom, Calendar now)
            throws DOMException {
        Element delivery = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableHeaderTradeDelivery");
        
        Element actual_delivery_supply_chain_event = dom
            .createElementNS(NAMESPACE_RAM, "ram:ActualDeliverySupplyChainEvent");
        delivery.appendChild(actual_delivery_supply_chain_event);
        
        append_date_time_string_element(dom, actual_delivery_supply_chain_event,
            NAMESPACE_RAM, "ram:OccurrenceDateTime", now);

        return delivery;
    }
}