Crear generador de facturas PDF en .NET

Categoría del blog: PDFOffice.NET

02.07.2021

Con frecuencia surge la tarea de crear una factura en formato PDF. Normalmente, el documento que representa la factura tiene un diseño complejo: un encabezado con el logotipo e información sobre la empresa, información sobre el vendedor y el comprador, una tabla con los artículos pedidos y un pie de página con información adicional.

Un documento PDF es un documento vectorial que no permite maquetar el contenido. Es posible escribir código de programación que dibuje el marcado del documento en la página PDF mediante gráficos vectoriales, pero el código sería complejo y su desarrollo podría llevar mucho tiempo.

Una tarea compleja se podría simplificar dividiéndola en dos partes:
1. Crear una plantilla de documento en un editor de texto.
2. Escribir el código de programación que llena la plantilla con datos dinámicos.

Un documento DOCX almacena el contenido en forma de marcado y, por lo tanto, puede usarse para crear una plantilla para facturas de cualquier complejidad.
Desde la versión 10.1 de VintaSoft Imaging .NET SDK es posible editar documentos DOCX y XLSX existentes mediante programación.
Se puede usar esta función para crear un generador de facturas simple y fácilmente personalizable en formato de documento PDF.

El generador de facturas debe contener dos partes principales:

Las ventajas de crear un generador de facturas basado en una plantilla de documento DOCX son:

Para más detalles sobre la edición programática de documentos DOCX, consulte aquí.


Para crear el generador de facturas, que genera la factura en formato PDF, siga estos pasos:
1. Cree una plantilla de factura Invoice_template.docx con MS Word, que contendrá todo el marcado estático de la factura.
2. Implementar en la aplicación el llenado de factura con datos dinámicos utilizando la clase DocxDocumentEditor.

Aquí está el código que muestra cómo crear una factura y guardarla en un documento PDF:
// 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;
}

El siguiente código crea los datos de prueba para el generador de facturas:
/// <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 LLC";

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