Create PDF invoice generator in .NET

Blog category: PDFOffice.NET

July 2, 2021

Quite often arises the task to create an invoice in PDF document format. Usually the document that represents the invoice contains a complex layout: a header with a logo and information about the company, information about the seller and the buyer, a table with ordered items, a footer with additional information.

PDF document is a vector document, which does not have the functionality for content layout. It is possible to write programming code, which draws the document markup on the PDF page using vector graphics, but the code would be complex and its development could take a long time.

A complex task could be simplified by dividing into 2 parts:
1. Creating a document template in text editor.
2. Writing the programming code that fills the template with dynamic data.

DOCX document stores content in the form of markup and therefore DOCX document can be used to create a template for invoice of any complexity.
Since version 10.1 of VintaSoft Imaging .NET SDK became possible to edit existing DOCX and XLSX documents programmatically.
One can use this feature to create a simple and easily customizable generator of invoices in PDF document format.

The invoice generator should contain two main parts:

The advantages of creating an invoice generator based on DOCX document template are:

More details about the editing of DOCX documents programmatically one can read here.


It is necessary to perform the following steps to create the invoice generator, which generates the invoice in PDF document format:
1. Create an invoice template Invoice_template.docx using MS Word, which would contain all static markup of the invoice.
2. Implement in the application the filling of invoice with dynamic data using DocxDocumentEditor class.

Here is the code showing how to create an invoice and save it to PDF document:
// The project, which uses this code, must have references to the following assemblies:
// - Vintasoft.Imaging
// - Vintasoft.Imaging.Office.OpenXml
// - Vintasoft.Imaging.Pdf
// - Vintasoft.Barcode

/// <summary>
/// Generates invoice, which is based on DOCX document template.
/// </summary>
public static void GenerateInvoiceUsingDocxTemplate()
{
    // create DOCX document editor and use file "Invoice_template.docx" as document template
    using (Vintasoft.Imaging.Office.OpenXml.Editor.DocxDocumentEditor editor =
        new Vintasoft.Imaging.Office.OpenXml.Editor.DocxDocumentEditor("Invoice_template.docx"))
    {
        // generate test invoice data with 30 items
        InvoiceData testData = GetTestData(30);

        // fill invoice data
        FillInvoiceData(editor, testData);

        // save invoice to a DOCX document if necessary
        //editor.Save("Invoice.docx");

        // export invoice to a PDF document
        editor.Export("Invoice_docx.pdf");
    }
}

/// <summary>
/// Fills the invoice data using DOCX document editor.
/// </summary>
/// <param name="documentEditor">The DOCX document editor.</param>
/// <param name="invoiceData">The invoice data.</param>
private static void FillInvoiceData(
    Vintasoft.Imaging.Office.OpenXml.Editor.DocxDocumentEditor documentEditor,
    InvoiceData invoiceData)
{
    // create QR code image 200x200 px
    using (Vintasoft.Imaging.VintasoftImage qrCodeImage = invoiceData.GetBarcodeImage(200))
    {
        // set barcode image to the image element at index 1
        documentEditor.Images[1].SetImage(qrCodeImage);
    }

    // fill the document header
    documentEditor.Body["[company_name]"] = invoiceData.Company.CompanyName;
    documentEditor.Body["[company_address]"] = invoiceData.Company.Address;
    documentEditor.Body["[company_city]"] = invoiceData.Company.City;
    documentEditor.Body["[company_phone]"] = invoiceData.Company.GetPhones();
    documentEditor.Body["[invoice_number]"] = invoiceData.InvoiceNumber;
    documentEditor.Body["[invoice_date]"] = System.DateTime.Now.ToShortDateString();

    // get all tables of document
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable[] tables = documentEditor.Tables;

    // fill the "Customer Information" table
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable customerInformationTable = tables[0];
    SetCompanyInformation(customerInformationTable, "billing", invoiceData.BillingAddress);
    SetCompanyInformation(customerInformationTable, "shipping", invoiceData.ShippingAddress);

    // fill the "Shipping Method" table
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable shippingMethodTable = tables[1];
    shippingMethodTable["[shipping_method]"] = invoiceData.ShippingMethod;

    // fill the "Order Information" table
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable orderInformationTable = tables[2];
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTableRow templateRow = orderInformationTable[1];
    int orderItemNumber = 1;
    // for each item in invoice
    foreach (InvoiceItem orderItem in invoiceData.OrderItems)
    {
        // copy template row and insert copy after template row
        Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTableRow currentRow = templateRow;
        templateRow = (Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTableRow)templateRow.InsertCopyAfterSelf();

        // fill data of current row
        currentRow["[p_n]"] = orderItemNumber.ToString();
        currentRow["[p_description]"] = orderItem.Product;
        currentRow["[p_qty]"] = orderItem.Quantity.ToString();
        currentRow["[p_unit_price]"] = invoiceData.GetPriceAsString(orderItem.Price);
        currentRow["[p_price_total]"] = invoiceData.GetPriceAsString(orderItem.TotalPrice);

        orderItemNumber++;
    }
    // remove template row
    templateRow.Remove();

    // fill order information summary fields
    orderInformationTable["[subtotal]"] = invoiceData.GetPriceAsString(invoiceData.Subtotal);
    orderInformationTable["[tax]"] = invoiceData.GetPriceAsString(invoiceData.Tax);
    orderInformationTable["[shipping]"] = invoiceData.GetPriceAsString(invoiceData.Shipping);
    orderInformationTable["[grand_total]"] = invoiceData.GetPriceAsString(invoiceData.GrandTotal);

    // fill the "Notes" table
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable notesTable = tables[3];
    notesTable["[date]"] = System.DateTime.Now.ToShortDateString();
    notesTable["[time]"] = System.DateTime.Now.ToLongTimeString();
}

/// <summary>
/// Sets the company information.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="fieldName">Name of the field.</param>
/// <param name="company">The company.</param>
private static void SetCompanyInformation(
    Vintasoft.Imaging.Office.OpenXml.Editor.OpenXmlDocumentTable table, 
    string fieldName, 
    Company company)
{
    string fieldFormat = string.Format("[{0}_{1}]", fieldName, "{0}");
    table[string.Format(fieldFormat, "company")] = company.CompanyName;
    table[string.Format(fieldFormat, "name")] = company.Name;
    table[string.Format(fieldFormat, "address")] = company.Address;
    table[string.Format(fieldFormat, "phone")] = company.GetPhones();
    table[string.Format(fieldFormat, "city")] = company.City;
}

The following code creates the test data for invoice generator:
/// <summary>
/// Returns the invoice test data.
/// </summary>
/// <returns>The invoice test data.</returns>
public static InvoiceData GetTestData(int orderItemsCount)
{
    Company vintasoftCompany = new Company();
    vintasoftCompany.CompanyName = "VintaSoft Ltd.";

    Company billingCompany = new Company();
    billingCompany.CompanyName = "Billing Global Company Inc.";
    billingCompany.Name = "Mr. Q";
    billingCompany.Address = "Address1";
    billingCompany.City = "City1";
    billingCompany.Phones.Add("9876543210");
    billingCompany.Phones.Add("7654321098 (fax)");

    Company shipingCompany = new Company();
    shipingCompany.CompanyName = "Shipping Global Company Inc.";
    shipingCompany.Name = "Mr. Z";
    shipingCompany.Address = "Address2";
    shipingCompany.City = "City2";
    shipingCompany.Phones.Add("1122334455");
    shipingCompany.Phones.Add("5544332211 (fax)");

    InvoiceData data = new InvoiceData();

    System.Random random = new System.Random();
    data.InvoiceNumber = string.Format("{0}-{1}", random.Next(100000, 999999), random.Next(0, 9));

    data.Company = vintasoftCompany;
    data.BillingAddress = billingCompany;
    data.ShippingAddress = shipingCompany;

    InvoiceItem[] availableProducts = new InvoiceItem[] {
        new InvoiceItem("VintaSoft Imaging .NET SDK, Site license for Desktop PCs", 659.95f),
        new InvoiceItem("VintaSoft Annotation .NET Plug-in, Site license for Desktop PCs", 449.95f),
        new InvoiceItem("VintaSoft Office .NET Plug-in, Site license for Desktop PCs", 569.95f),
        new InvoiceItem("VintaSoft PDF .NET Plug-in (Reader+Writer), Site license for Desktop PCs", 1499.95f),
        new InvoiceItem("VintaSoft PDF .NET Plug-in (Reader+Writer+VisualEditor), Site license for Desktop PCs", 2999.95f),
        new InvoiceItem("VintaSoft JBIG2 .NET Plug-in, Site license for Desktop PCs", 1139.95f),
        new InvoiceItem("VintaSoft JPEG2000 .NET Plug-in, Site license for Desktop PCs", 689.95f),
        new InvoiceItem("VintaSoft Document Cleaup .NET Plug-in, Site license for Desktop PCs", 569.95f),
        new InvoiceItem("VintaSoft OCR .NET Plug-in, Site license for Desktop PCs", 509.95f),
        new InvoiceItem("VintaSoft DICOM .NET Plug-in (Codec+MPR), Site license for Desktop PCs", 1199.95f),
        new InvoiceItem("VintaSoft Forms Processing .NET Plug-in, Site license for Desktop PCs", 509.95f),
        new InvoiceItem("VintaSoft Barcode .NET SDK (1D+2D Reader+Writer), Site license for Desktop PCs", 1379.95f),
        new InvoiceItem("VintaSoft Twain .NET SDK, Site license", 539.95f)
    };

    for (int i = 0; i < orderItemsCount; i++)
    {
        int quantity = 1 + random.Next(10);
        int index = random.Next(availableProducts.Length - 1);
        data.OrderItems.Add(new InvoiceItem(availableProducts[index], quantity));
    }

    return data;
}

/// <summary>
/// Represents an information about company.
/// </summary>
public class Company
{

    /// <summary>
    /// The company name.
    /// </summary>
    public string CompanyName;

    /// <summary>
    /// The person name.
    /// </summary>
    public string Name;

    /// <summary>
    /// The company location city.
    /// </summary>
    public string City;

    /// <summary>
    /// The company location adress.
    /// </summary>
    public string Address;

    /// <summary>
    /// The company phone numbers.
    /// </summary>
    public System.Collections.Generic.List<string> Phones = new System.Collections.Generic.List<string>();



    /// <summary>
    /// Returns the phone numbers.
    /// </summary>
    public string GetPhones()
    {
        if (Phones.Count == 1)
            return Phones[0];
        System.Text.StringBuilder result = new System.Text.StringBuilder();
        for (int i = 0; i < Phones.Count - 1; i++)
        {
            result.Append(Phones[i]);
            result.Append(", ");
        }
        result.Append(Phones[Phones.Count - 1]);
        return result.ToString();
    }

}

/// <summary>
/// Represents an invoice order item.
/// </summary>
public class InvoiceItem
{

    /// <summary>
    /// Initializes a new instance of the <see cref="InvoiceItem"/> class.
    /// </summary>
    /// <param name="product">The product name.</param>
    /// <param name="price">The product price.</param>
    public InvoiceItem(string product, float price)
    {
        Product = product;
        Quantity = 1;
        Price = price;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="InvoiceItem"/> class.
    /// </summary>
    /// <param name="source">The source <see cref="InvoiceItem"/>.</param>
    /// <param name="quantity">The product quantity.</param>
    public InvoiceItem(InvoiceItem source, float quantity)
    {
        Product = source.Product;
        Price = source.Price;
        Quantity = quantity;
    }



    /// <summary>
    /// The product name.
    /// </summary>
    public string Product;

    /// <summary>
    /// The product quantity.
    /// </summary>
    public float Quantity;

    /// <summary>
    /// The product price.
    /// </summary>
    public float Price;



    /// <summary>
    /// Gets the product total price.
    /// </summary>
    public float TotalPrice
    {
        get
        {
            return Price * Quantity;
        }
    }

}

/// <summary>
/// Represents an invoice data.
/// </summary>
public class InvoiceData
{

    /// <summary>
    /// The list of order items.
    /// </summary>
    public System.Collections.Generic.List<InvoiceItem> OrderItems = new System.Collections.Generic.List<InvoiceItem>();

    /// <summary>
    /// The invoice number.
    /// </summary>
    public string InvoiceNumber;

    /// <summary>
    /// The shipping method.
    /// </summary>
    public string ShippingMethod = "Email";

    /// <summary>
    /// The company billing address.
    /// </summary>
    public Company BillingAddress = new Company();

    /// <summary>
    /// The company shipping address.
    /// </summary>
    public Company ShippingAddress = new Company();

    /// <summary>
    /// The object that represents information about the company.
    /// </summary>
    public Company Company = new Company();

    /// <summary>
    /// The currency to be used in the invoice.
    /// </summary>
    public string Currency = "EUR";

    /// <summary>
    /// Gets or sets the tax value.
    /// </summary>
    public float Tax = 0;

    /// <summary>
    /// Gets or sets the shipping price.
    /// </summary>
    public float Shipping = 0;



    /// <summary>
    /// Gets the subtotal value.
    /// </summary>
    public float Subtotal
    {
        get
        {
            float value = 0;
            for (int i = 0; i < OrderItems.Count; i++)
                value += OrderItems[i].TotalPrice;
            return value;
        }
    }


    /// <summary>
    /// Gets the grandtotal value.
    /// </summary>
    public float GrandTotal
    {
        get
        {
            return Subtotal + Shipping + Tax;
        }
    }



    /// <summary>
    /// Returns the price as a string.
    /// </summary>
    /// <param name="price">The price.</param>
    /// <returns>The price in string representation.</returns>
    public string GetPriceAsString(float price)
    {
        return string.Format("{0} {1}", price.ToString("f2", System.Globalization.CultureInfo.InvariantCulture), Currency);
    }

    /// <summary>
    /// Creates the QR code image.
    /// </summary>
    /// <param name="size">The barcode size.</param>
    /// <returns>An instance o f<see cref="Vintasoft.Imaging.VintasoftImage"/> class that contains QR code image.</returns>
    public Vintasoft.Imaging.VintasoftImage GetBarcodeImage(int size)
    {
        Vintasoft.Barcode.BarcodeWriter writer = new Vintasoft.Barcode.BarcodeWriter();

        writer.Settings.Barcode = Vintasoft.Barcode.BarcodeType.QR;

        writer.Settings.Value = string.Format("INVOICE={0};TOTAL={1}", InvoiceNumber, GetPriceAsString(GrandTotal));

        writer.Settings.SetWidth(size);

        Vintasoft.Imaging.VintasoftImage result = 
            new Vintasoft.Imaging.VintasoftImage(writer.GetBarcodeAsBitmap(), true);
        result.Crop(new System.Drawing.Rectangle(0, 0, result.Width, result.Width));

        return result;
    }
}