PDFlib Cookbook

cookbook

pdfa/zugferd_invoice

Download Java Code      Show Output PDF      Show Input File

/*
 * $Id: zugferd_invoice.java,v 1.16 2016/01/13 10:28:30 stm Exp $
 *
 * Create a PDF/A-3b ZUGFeRD 1.0 invoice from scratch.
 * 
 * This program is based on information released in the "ZUGFeRD-Infopaket"
 * by FeRD:
 * 
 * http://www.ferd-net.de/
 *
 * The XML invoice uses the ZUGFeRD profile "BASIC".
 *
 * Required software: PDFlib+PDI/PPS 9.0.1 or later
 * (set run_sample_with_pdi=false below to run the sample code without PDI)
 * Required data: two fonts, stationery PDF
 */

package com.pdflib.cookbook.pdflib.pdfa;

import java.io.StringWriter;
import java.math.BigDecimal;
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.Comment;
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 for validating the generated XML against the
 * ZUGFeRD XML schema.
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 zugferd_invoice {
    private static final String COMMENT = "Created by PDFlib Cookbook program "
        + "zugferd_invoice.java $Revision: 1.16 $\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 = "DejaVuSerif";

    final static String basefontoptions = "fontname=" + fontname + " fontsize="
        + fontsize + " encoding=unicode embedding";

    /*
     * XML namespaces used in the invoice document.
     */
    final static String NAMESPACE_RSM =
        "urn:ferd:CrossIndustryDocument:invoice:1p0";  
    final static String NAMESPACE_RAM =
        "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12";
    final static String NAMESPACE_UDT =
        "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15";

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

    /**
     * The German VAT percentage.
     */
    private static double VAT_PERCENT = 19;

    /**
     * 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);

    /**
     * Place company stationery.
     * 
     * @throws Exception
     */
    static void create_stationery(pdflib p) throws Exception {
        String sender = seller.name + " • " + seller.street
            + " • " + seller.postcode + " " + seller.city + " • "
            + seller.country;
        String stationeryfontname = "LuciduxSans-Oblique";

        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";

        String payment_terms =
            "Zahlbar innerhalb von 30 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 = "zugferd_invoice.pdf";
        String title = "ZUGFeRD Rechnung";
        String xmpfile = "ZUGFeRD_extension_schema.xmp";
        String xml_invoice_file = "ZUGFeRD-invoice.xml";
        String invoice_number = calendar.get(Calendar.YEAR) + "-03";
        String document_name = "Rechnung";
        String customer_number = "987-654";
        String order_number = "ABC-123";

        /*
         * 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 ZUGFeRD XML invoice.
             */
            Document dom = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder().newDocument();

            Element root = xml_create_root(dom);

            Comment comment = dom.createComment(COMMENT);
            root.appendChild(comment);

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

            Element document_header = xml_create_document_header(dom,
                document_name, invoice_number, calendar);
            root.appendChild(document_header);

            Element supply_chain_trade_transaction = dom.createElementNS(
                NAMESPACE_RSM, "rsm:SpecifiedSupplyChainTradeTransaction");
            root.appendChild(supply_chain_trade_transaction);

            Element supply_chain_trade_agreement =
                xml_create_applicable_supply_chain_trade_agreement(
                    dom, seller, buyer);
            supply_chain_trade_transaction
                .appendChild(supply_chain_trade_agreement);

            Element delivery =
                xml_create_applicable_supply_chain_trade_delivery(dom, calendar);
            supply_chain_trade_transaction
                .appendChild(delivery);
            
            p = new pdflib();

            String optlist = "SearchPath=" + searchpath;
            p.set_option(optlist);

            /*
             * 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", "zugferd_invoice.java");
            p.set_info("Title", title + " $Revision: 1.16 $");

            /* Use sRGB output intent so that we can use RGB color */
            p.load_iccprofile("sRGB", "usage=outputintent");

            /*
             * Need to add the ram:ApplicableSupplyChainTradeSettlement
             * element before the ram:IncludedSupplyChainTradeLineItems,
             * therefore calculate total up-front.
             */
            double total = 0;
            int i;
            for (i = 0; i < data.length; i++) {
                double sum = data[i].price * data[i].quantity;
                total += sum;
            }
            
            BigDecimal bd_netto = new BigDecimal(total).setScale(2,
                BigDecimal.ROUND_HALF_UP);
            BigDecimal bd_vat_pct = new BigDecimal(VAT_PERCENT / 100);
            BigDecimal bd_vat = bd_netto.multiply(bd_vat_pct).setScale(2,
                BigDecimal.ROUND_HALF_UP);
            BigDecimal bd_brutto = bd_netto.add(bd_vat).setScale(2,
                BigDecimal.ROUND_HALF_UP);
            
            Element supply_chain_trade_settlement =
                xml_create_supply_chain_trade_settlement(
                    dom, invoice_number, payment_terms,
                    bd_netto, bd_vat);
            supply_chain_trade_transaction
                .appendChild(supply_chain_trade_settlement);
            
            /*
             * 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++;

            /* Data rows: one for each article */
            for (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, BigDecimal.ROUND_HALF_UP),
                    new BigDecimal(sum).setScale(2, BigDecimal.ROUND_HALF_UP));
                supply_chain_trade_transaction.appendChild(line_item);

                row++;
            }

            /* 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 ZUGFeRD
             * ApplicableSupplyChainTradeSettlement element.
             */
            Element monetary_summation = xml_create_monetary_summation(dom,
                bd_netto, bd_vat, bd_brutto);
            supply_chain_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/ZUGFeRD_1p0.xsd" with the actual
             * pathname to the ZUGFeRD XML schema, 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/ZUGFeRD_1p0.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". Also mark it as an
             * attachment that can be retrieved with Acrobat's normal attachment
             * dialog.
             */
            int xml_invoice = p.load_asset("Attachment", pvf_name,
                "mimetype=text/xml "
                    + "description={Rechnungsdaten im ZUGFeRD-XML-Format} "
                    + "relationship=Alternative documentattachment=true");

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

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

    /**
     * Create an "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 "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");

        /*
         * The Schematron validation wants an empty
         * "ram:AssociatedDocumentLineDocument" element without a "ram:LineID"
         * child element for a BASIC invoice.
         */
        Element line_document = dom
            .createElementNS(NAMESPACE_RAM, "ram:AssociatedDocumentLineDocument");
        line_item.appendChild(line_document);

        Element delivery = dom
            .createElementNS(NAMESPACE_RAM, "ram:SpecifiedSupplyChainTradeDelivery");
        Element billed_quantity = append_text_element(dom, delivery,
            NAMESPACE_RAM, "ram:BilledQuantity", quantity);
        /*
         * C62 means "one item".
         */
        billed_quantity.setAttribute("unitCode", "C62");
        delivery.appendChild(billed_quantity);
        line_item.appendChild(delivery);

        /*
         * The Schematron validation wants an empty
         * "ram:SpecifiedSupplyChainTradeSettlement" element without a
         * "ram:SpecifiedTradeSettlementMonetarySummation" child element for
         * a BASIC invoice.
         */
        Element trade_settlement = dom
            .createElementNS(NAMESPACE_RAM, "ram:SpecifiedSupplyChainTradeSettlement");
        line_item.appendChild(trade_settlement);

        Element product = dom.createElementNS(NAMESPACE_RAM, "ram:SpecifiedTradeProduct");
        append_text_element(dom, product, NAMESPACE_RAM, "ram:Name", name);
        line_item.appendChild(product);

        return line_item;
    }

    /**
     * Create a "SpecifiedTradeSettlementMonetarySummation" XML element.
     * 
     * @param dom
     *            XML document
     * @param netto
     *            Netto price
     * @param vat
     *            Calculated VAT sum
     * @param brutto
     *            Brutto price (netto + vat)
     * 
     * @return New "SpecifiedTradeSettlementMonetarySummation" 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:SpecifiedTradeSettlementMonetarySummation");

        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:LineTotalAmount", netto);
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:ChargeTotalAmount", new BigDecimal(0));
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:AllowanceTotalAmount", new BigDecimal(0));
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:TaxBasisTotalAmount", netto);
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:TaxTotalAmount", vat);
        append_currency_element(dom, monetary_summation,
            NAMESPACE_RAM, "ram:GrandTotalAmount", brutto);

        return monetary_summation;
    }

    /**
     * Create "ApplicableSupplyChainTradeSettlement" XML element.
     * 
     * @param dom
     *            XML document
     * @param invoice_number
     *            Invoice number
     * @param payment_terms
     *            Payment terms (freetext)
     * @param netto
     *            Net sum
     * @param vat
     *            Calculated VAT
     * 
     * @return New "ApplicableSupplyChainTradeSettlement" XML element
     */
    private static Element xml_create_supply_chain_trade_settlement(
        Document dom, String invoice_number, String payment_terms,
        BigDecimal netto, BigDecimal vat) {
        Element settlement = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableSupplyChainTradeSettlement");

        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:ApplicablePercent",
            Double.toString(VAT_PERCENT));
        settlement.appendChild(applicable_trade_tax);

        return settlement;
    }

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

        /*
         * Do not mistake this for a real invoice...
         */
        Element test_indicator_element =
            dom.createElementNS(NAMESPACE_RAM, "ram:TestIndicator");
        append_text_element(dom, test_indicator_element, NAMESPACE_UDT, 
                                                "udt:Indicator", "true");
        document_context.appendChild(test_indicator_element);
        
        Element context_param = dom
            .createElementNS(NAMESPACE_RAM,
                "ram:GuidelineSpecifiedDocumentContextParameter");
        document_context.appendChild(context_param);

        append_text_element(dom, context_param, NAMESPACE_RAM, "ram:ID",
            "urn:ferd:CrossIndustryDocument:invoice:1p0:basic");

        return document_context;
    }

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

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

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

        /*
         * UNCL 1001 Document Name Code; in BASIC und COMFORT only 380:
         * ("Gutschrift im Sinne von Rechnungskorrekturen mit negativen Werten")
         */
        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(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.
         */
        Element new_element = append_text_element(dom, parent, namespace_uri,
            element_name, value.toPlainString());
        new_element.setAttribute("currencyID", CURRENCY);

        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 ZUGFeRD XML schema.
         */
        Element root = dom.createElementNS(NAMESPACE_RSM, "rsm:CrossIndustryDocument");
        
        root.setAttribute("xmlns:xsi",
            "http://www.w3.org/2001/XMLSchema-instance");
        root.setAttribute("xmlns:udt", NAMESPACE_UDT);
        root.setAttribute("xmlns:rsm", NAMESPACE_RSM);
        root.setAttribute("xmlns:ram", NAMESPACE_RAM);
        
        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 "ApplicableSupplyChainTradeAgreement" XML element.
     * 
     * @param dom
     *            XML document
     * @param seller
     *            Seller information
     * @param buyer
     *            Buyer information
     * 
     * @return The new "ApplicableSupplyChainTradeAgreement" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_applicable_supply_chain_trade_agreement(
        Document dom, trade_party seller, trade_party buyer)
        throws DOMException {
        Element agreement = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableSupplyChainTradeAgreement");

        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);

        return agreement;
    }
    
    /**
     * Create an "ApplicableSupplyChainTradeAgreement" XML element.
     * 
     * @param dom
     *            XML document
     * @param seller
     *            Seller information
     * @param buyer
     *            Buyer information
     * 
     * @return The new "ApplicableSupplyChainTradeAgreement" XML element
     * 
     * @throws DOMException
     */
    private static Element xml_create_applicable_supply_chain_trade_delivery(
        Document dom, Calendar now)
            throws DOMException {
        Element delivery = dom
            .createElementNS(NAMESPACE_RAM, "ram:ApplicableSupplyChainTradeDelivery");
        
        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;
    }
}