Créer un générateur de factures PDF en .NET

Catégorie du blog: PDFOffice.NET

02.07.2021

Il est fréquent de devoir créer une facture au format PDF. Généralement, un document de facture présente une mise en page complexe: un en-tête avec un logo et des informations sur l’entreprise, des informations sur le vendeur et l’acheteur, un tableau récapitulatif des articles commandés et un pied de page avec des informations complémentaires.

Un document PDF est un document vectoriel, qui ne permet pas de gérer la mise en page du contenu. Il est possible d’écrire un code qui dessine le balisage du document sur la page PDF à l’aide de graphiques vectoriels, mais ce code serait complexe et son développement pourrait être long.

Une tâche complexe peut être simplifiée en la divisant en deux parties:
1. Création d'un modèle de document dans un éditeur de texte.
2. Écriture du code de programmation qui remplit le modèle avec des données dynamiques.

Un document DOCX stocke le contenu sous forme de balisage et peut donc être utilisé pour créer un modèle de facture de toute complexité.
Depuis la version 10.1 du VintaSoft Imaging .NET SDK, il est possible de modifier par programmation des documents DOCX et XLSX existants.
Cette fonctionnalité permet de créer un générateur de factures au format PDF simple et facilement personnalisable.

Le générateur de factures doit contenir deux parties principales:

Les avantages de la création d'un générateur de factures basé sur un modèle de document DOCX sont:

Pour plus de détails sur l'édition programmatique de documents DOCX, veuillez consulter ici.


Il est nécessaire de suivre les étapes suivantes pour créer le générateur de factures, qui génère la facture au format PDF:
1. Créez un modèle de facture Invoice_template.docx à l'aide de MS Word, contenant toute la mise en forme statique de la facture.
2. Implémentez dans l'application le remplissage de la facture avec des données dynamiques en utilisant la classe DocxDocumentEditor.

Voici le code montrant comment créer une facture et l'enregistrer au format 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;
}

Le code suivant crée les données de test pour le générateur de factures:
/// <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;
    }
}