Generating DOCX and PDF Invoices in C# with NOV Rich Text Editor

Introduction

Invoice and quote generation is one of the most common applications for automated document generation. This is because it usually involves looping through a SQL database or Excel spreadsheet containing client and order data and generating the numbered/dated invoice or quote that has to be sent to the client. Such automatic invoice generation systems are present in almost every company that has a lot of clients or processes many orders.

Invoices and quotes typically consist of text content in the form of paragraphs and tables and as a general rule contain the following:

  • Invoice or quote number and date
  • Seller's name, address, EIN, VAT number (if in the EU), etc. along with payment details.
  • Customer’s name, address, and tax/VAT number.
  • Detailed description of goods, services, along with their quantity and price.
  • If applicable, shipping and tax charges.
  • Other items such as notes, terms, or legal statements, etc.


Optionally, such documents contain images with the company logo, preferably in the form of scalable content such as EMF or SVG files, because they retain quality when exported to PDF or printed, which is very common for invoices.

Finally, in the case of PDF invoices, the generated content must contain a subset of the font glyphs (only the used ones) so that the invoice renders identically on computers that do not contain the fonts used in the invoice design and content. Therefore, a good turnkey solution for automatic invoice generation must be able to support all those features out of the box. The current article outlines the steps and code needed to create a fully automated invoice generator using C#. The complete code is available as part of the free examples shipped with Nevron Open Vision examples. Following is an image of the sample generated PDF invoice: Sample Invoice

Define the Invoice Data

The first step when designing an automatic invoice generator is to define a model that represents the invoice data. This keeps data acquisition separate from document formatting and makes the generator easy to use with SQL, a Web API, an Excel worksheet, or any other data source. This example uses immutable records for the parties, invoice lines, and invoice data itself:

public sealed record Party(string Name, string Address1, string Address2,
    string Country, string Ein, string Email = "", string Website = "");

public sealed record InvoiceLine(string Code, string Description, string Period,
    decimal Quantity, string Unit, decimal UnitPrice);

public sealed record InvoiceData(
    string Number, DateTime IssueDate, DateTime TaxPointDate, DateTime DueDate,
    string CustomerReference, Party Customer, Party Supplier,
    IReadOnlyList<InvoiceLine> Lines, decimal SalesTaxRate, string Currency,
    string BankName, string RoutingNumber, string AccountNumber,
    string AccountType, string PreparedBy, string PaymentUrl);
This simple model does not model the Supplier and Customer as different records, but more advanced models can make this distinction.

var supplier = new Party(
    Name: "BluePeak Cloud Services, Inc.",
    Address1: "250 Cloud Avenue",
    Address2: "Seattle, WA 98101",
    Country: "United States",
    Ein: "91-7463058",
    Email: "billing@bluepeak.example",
    Website: "https://bluepeak.example/");

// Pass supplier to the Supplier argument when constructing InvoiceData.
These are fictional contact details for the sample invoice. Monetary values are represented with decimal, which allows you to avoid floating/double precision loss when dealing with fractional numbers.

Create and Configure the Document

To create and configure the document object that is exported as an invoice, you first need to create an NRichTextView in the application and pass it together with the invoice data to the invoice document generator. The CreatePdf method clears the current content and builds a new invoice document based on the passed invoice data:

public static void CreatePdf(NRichTextView view, InvoiceData invoice)
{
    view.Content.Sections.Clear();

    var section = new NSection
    {
        PageSize = new NPaperSize(ENPaperKind.A4),
        PageMargins = new NMargins(36, 36, 36, 42)
    };
    view.Content.Sections.Add(section);

    AddHeader(section, invoice.Supplier);
    AddFooter(section);
    AddSupplierAndCustomer(section, invoice);
    AddInvoiceIdentity(section, invoice);
    AddLineItems(section, invoice);
    AddTotals(section, invoice);
    AddPaymentSection(section, invoice);
}
Keeping each logical part in a separate method makes it possible to change one part of the invoice without affecting the remaining document structure.

Add the Repeating Header and Footer

Headers and footers belong to the section and repeat automatically when the invoice spans multiple pages. The example creates NHeaderFooter instances and assigns them to the section's Header and Footer properties.

private static void AddHeader(NSection section, Party supplier)
{
    NHeaderFooter header = new NHeaderFooter();
    header.Blocks.Add(new NParagraph(supplier.Name));

    section.Header = header;
}
The repeating header uses the supplier's name, so the BluePeak sample remains consistently branded on subsequent pages. The footer in the complete source uses a two-column table. The left cell contains the electronic-invoice notice, while the right cell contains the page indicator.

Add Supplier and Customer Information

The AddSupplierAndCustomer method builds two tables. The first table displays only a subset of the supplier information (name, website and email). As mentioned, more advanced data models may model the supplier and customer data differently and for example add support for a company logo and other branding information which can then be inserted into the invoice. The second table places the customer information on the left and the supplier's on the right. The tables use percentage column widths to ensure that the layout scales with the printable page width.

Note how the code adds information from the party record to supplier/customer tables:

brandParagraph.Inlines.Add(new NTextInline(x.Supplier.Name)
{
    FontSize = 15,
    FontStyle = ENFontStyle.Bold
});
emailParagraph.Inlines.Add(new NTextInline(x.Supplier.Email)
{
    FontSize = 10,
    FontStyle = ENFontStyle.Regular
});
websiteParagraph.Inlines.Add(new NTextInline(x.Supplier.Website)
{
    FontSize = 10,
    FontStyle = ENFontStyle.Regular
});
These excerpts show how to outline important information on the invoice by changing its formatting. The table containing customer/supplier information is created as follows:

var parties = new NTable(0, 2)
{
    Border = NBorder.CreateFilledBorder(NColor.LightGray, 0.5, 0.5)
};

var partiesRow = new NTableRow();
Party[] partyValues = { x.Customer, x.Supplier };
string[] captions = { "CUSTOMER:", "SUPPLIER:" };
ENAlign[] alignments = { ENAlign.Left, ENAlign.Right };
Each party is converted to a sequence of paragraphs containing its name, address, country, and EIN. Applying alignment to the paragraphs keeps the customer block left-aligned and the supplier block right-aligned. The full code of the AddSupplierAndCustomer method is:
								
private static void AddSupplierAndCustomer(NSection section, InvoiceData x)
{
    var contact = new NTable(0, 2)
    {
        Border = NBorder.CreateFilledBorder(NColor.LightGray, 0.5, 0.5)
    };
    contact.Columns[0].PreferredWidth =
        new NMultiLength(ENMultiLengthUnit.Percentage, 65);
    contact.Columns[1].PreferredWidth =
        new NMultiLength(ENMultiLengthUnit.Percentage, 35);

    var contactRow = new NTableRow();

    var brandCell = new NTableCell { Padding = new NMargins(5) };
    var brandParagraph = new NParagraph
    {
        HorizontalAlignment = ENAlign.Left,
        Margins = new NMargins(0, 0, 0, 1)
    };
    brandParagraph.Inlines.Add(new NTextInline(x.Supplier.Name)
    {
        FontSize = 15,
        FontStyle = ENFontStyle.Bold
    });
    brandCell.Blocks.Add(brandParagraph);

    var sloganParagraph = new NParagraph
    {
        HorizontalAlignment = ENAlign.Left,
        Margins = new NMargins(0, 0, 0, 1)
    };
    sloganParagraph.Inlines.Add(new NTextInline("Global expertise, local outcomes")
    {
        FontSize = 15,
        FontStyle = ENFontStyle.Bold
    });
    brandCell.Blocks.Add(sloganParagraph);
    contactRow.Cells.Add(brandCell);

    var contactCell = new NTableCell { Padding = new NMargins(5) };
    var emailParagraph = new NParagraph
    {
        HorizontalAlignment = ENAlign.Right,
        Margins = new NMargins(0, 0, 0, 1)
    };
    emailParagraph.Inlines.Add(new NTextInline(x.Supplier.Email)
    {
        FontSize = 10,
        FontStyle = ENFontStyle.Regular
    });
    contactCell.Blocks.Add(emailParagraph);

    var websiteParagraph = new NParagraph
    {
        HorizontalAlignment = ENAlign.Right,
        Margins = new NMargins(0, 0, 0, 1)
    };
    websiteParagraph.Inlines.Add(new NTextInline(x.Supplier.Website)
    {
        FontSize = 10,
        FontStyle = ENFontStyle.Regular
    });
    contactCell.Blocks.Add(websiteParagraph);
    contactRow.Cells.Add(contactCell);

    contact.Rows.Add(contactRow);
    section.Blocks.Add(contact);
    section.Blocks.Add(new NParagraph
    {
        Margins = new NMargins(0, 0, 0, 14)
    });

    var parties = new NTable(0, 2)
    {
        Border = NBorder.CreateFilledBorder(NColor.LightGray, 0.5, 0.5)
    };
    var partiesRow = new NTableRow();

    Party[] partyValues = { x.Customer, x.Supplier };
    string[] captions = { "CUSTOMER:", "SUPPLIER:" };
    ENAlign[] alignments = { ENAlign.Left, ENAlign.Right };

    for (int i = 0; i < partyValues.Length; i++)
    {
        Party party = partyValues[i];
        ENAlign alignment = alignments[i];
        var partyCell = new NTableCell { Padding = new NMargins(5) };

        string[] lines =
        {
            captions[i], party.Name, party.Address1, party.Address2,
            party.Country, $"EIN: {party.Ein}"
        };

        for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
        {
            var paragraph = new NParagraph
            {
                HorizontalAlignment = alignment,
                Margins = new NMargins(0, 0, 0, 1)
            };
            paragraph.Inlines.Add(new NTextInline(lines[lineIndex])
            {
                FontSize = 10,
                FontStyle = lineIndex < 2
                    ? ENFontStyle.Bold
                    : ENFontStyle.Regular
            });
            partyCell.Blocks.Add(paragraph);
        }

        partiesRow.Cells.Add(partyCell);
    }

    parties.Rows.Add(partiesRow);
    section.Blocks.Add(parties);
    section.Blocks.Add(new NParagraph
    {
        Margins = new NMargins(0, 0, 0, 16)
    });
}

Add the Invoice Identity and Dates

The AddInvoiceIdentity method creates the centered invoice title, original-document label, invoice number, and issue date. A three-column metadata table then displays the tax-point date, due date, and customer reference.

var number = new NParagraph
{
    HorizontalAlignment = ENAlign.Center,
    Margins = new NMargins(0, 0, 0, 1)
};
number.Inlines.Add(new NTextInline(
    $"No. {x.Number} / {x.IssueDate:dd.MM.yyyy}")
{
    FontSize = 12,
    FontStyle = ENFontStyle.Bold
});
section.Blocks.Add(number);
Using separate NTextInline objects makes font size and font style explicit and allows additional text runs with different formatting to be added later.

Build the Line-Item Table

The line-item area is a six-column NTable containing the line number, description, quantity, unit, unit price, and amount. Column widths are expressed as percentages to ensure the table layout stays consistent relative to the invoice page size if it changes:

var table = new NTable(0, 6)
{
    Border = NBorder.CreateFilledBorder(NColor.LightGray, 0.5, 0.5)
};

double[] widths = { 4, 49, 8, 9, 15, 15 };
for (int i = 0; i < widths.Length; i++)
{
    table.Columns[i].PreferredWidth =
        new NMultiLength(ENMultiLengthUnit.Percentage, widths[i]);
}
Afterward, the code loops through the headers:
for (int i = 0; i < headers.Length; i++)
{
    var cell = new NTableCell
    {
        Padding = new NMargins(5),
        BackgroundFill = new NColorFill(HeaderGray)
    };
    var paragraph = new NParagraph
    {
        HorizontalAlignment = headerAlignments[i],
        Margins = new NMargins(0, 0, 0, 1)
    };
    paragraph.Inlines.Add(new NTextInline(headers[i])
    {
        FontSize = 10,
        FontStyle = ENFontStyle.Bold
    });
    cell.Blocks.Add(paragraph);
    headerRow.Cells.Add(cell);
}
table.Rows.Add(headerRow);
								
and inserts each header as a table cell, by setting the font style to bold and applying a gray background. This is done to give the reader of the invoice a visual distinction between the header and the line-item rows. The method then loops through InvoiceData.Lines, calculates each line amount from quantity and unit price, and inserts the formatted values into a new row:
for (int lineIndex = 0; lineIndex < x.Lines.Count; lineIndex++)
{
    InvoiceLine line = x.Lines[lineIndex];
    decimal amount = line.Quantity * line.UnitPrice;
    string[] values =
    {
        (lineIndex + 1).ToString(En),
        $"{line.Code}\n{line.Description}\n{line.Period}",
        line.Quantity.ToString("N2", En),
        line.Unit,
        $"{line.UnitPrice.ToString("N2", En)} {x.Currency}",
        $"{amount.ToString("N2", En)} {x.Currency}"
    };
    ENAlign[] alignments =
    {
        ENAlign.Left, ENAlign.Left, ENAlign.Right,
        ENAlign.Left, ENAlign.Right, ENAlign.Right
    };

    var row = new NTableRow();
    for (int columnIndex = 0; columnIndex < values.Length; columnIndex++)
    {
        var cell = new NTableCell { Padding = new NMargins(5) };
        foreach (string textLine in values[columnIndex].Split('\n'))
        {
            var paragraph = new NParagraph
            {
                HorizontalAlignment = alignments[columnIndex],
                Margins = new NMargins(0, 0, 0, 1)
            };
            paragraph.Inlines.Add(new NTextInline(textLine)
            {
                FontSize = 10,
                FontStyle = ENFontStyle.Regular
            });
            cell.Blocks.Add(paragraph);
        }
        row.Cells.Add(cell);
    }
    table.Rows.Add(row);
}	
Descriptions can contain several paragraphs, which is useful for product codes, service names, and billing periods.

Calculate and Display Totals

The AddTotals method calculates the subtotal, optional sales tax, and final amount with decimal arithmetic. All values use rounding up to the second decimal number.

decimal net = 0;
foreach (InvoiceLine line in x.Lines)
{
    net += line.Quantity * line.UnitPrice;
}

decimal salesTax = Math.Round(net * x.SalesTaxRate, 2);
decimal gross = net + salesTax;
Sales-tax rows are added only when SalesTaxRate is greater than zero. The complete implementation places the totals table on the right side of a two-column outer table and highlights the final amount cells with a gray background.

Add Payment Details and a QR Code

The payment section contains US banking information, the amount due, the preparer's name, and a QR code. NOV Barcode widgets can be embedded directly in rich-text content through NWidgetInline, so the QR code participates in document layout and export without first being converted to an external bitmap.

var qr = new NMatrixBarcode(
    ENMatrixBarcodeSymbology.QrCode, x.PaymentUrl)
{
    ErrorCorrection = ENErrorCorrectionLevel.Medium,
    PreferredSize = new NSize(92, 92)
};

var qrParagraph = new NParagraph
{
    HorizontalAlignment = ENAlign.Center
};
qrParagraph.Inlines.Add(new NWidgetInline(qr));
The surrounding three-column table shows bank details, payment summary, and the QR code containing the payment link. The source uses imaginary account information and an imaginary payment URL for demonstration purposes.

Generate DOCX and PDF Files

After the generator has populated the NRichTextView, you can save the document to a file or directly stream it to a passed stream. The following code shows how to export the generated invoice to a DOCX or PDF file:

NRichTextView richTextView = new NRichTextView();

NovInvoiceGenerator.CreatePdf(richTextView, invoice);

richTextView.SaveToLocalFile(@"C:\Invoices\INV-2026-1042.docx");
richTextView.SaveToLocalFile(@"C:\Invoices\INV-2026-1042.pdf");

Invoice Integrity

Finally, after you create the generator, you need to verify if the generated document is correct. This involves checking both the data model and the accuracy of the generated visual representation, as it is very common that even if the data provided by the business logic is correct there are issues with its formatting. The usual things to check are:
  • Whether the calculated totals are correct.
  • If the invoice number and date are correct and if they match the internal accounting rules.
  • If the supplier and customer information is correctly displayed.

Conclusion

NOV Rich Text Editor allows .NET application developers to easily produce automatic invoices in DOCX and PDF formats. The code presented in the article can be used in desktop applications, web applications, background services, and scheduled invoice-generation jobs, and will work without modification on any .NET-compatible environment supported by NOV.

About Nevron Software

Founded in 1998, Nevron Software is a component vendor specializing in the development of premium presentation layer solutions for .NET-based technologies. Today, Nevron has established itself as a trusted partner worldwide for .NET LOB applications, SharePoint portals, and reporting solutions. Nevron technology is used by many Fortune 500 companies, large financial institutions, global IT consultancies, academic institutions, governments, and non-profits.
For more information, visit: www.nevron.com.
  
Nevron Chart for WinForms is a first-class product. Its wide range of possibilities, looks, settings and other options gives you a very powerful tool to support your own application and to make end user a superb presenter. This product’s price/performance ratio is the best in its segment. It seems that only limitation is the one from the user.
  

Bostjan Maljevec
Eunomia