ZugferdInvoice.vb
''
'' This code is part of Document Solutions for PDF demos.
'' Copyright (c) MESCIUS inc. All rights reserved.
''
Imports System.IO
Imports System.Drawing
Imports System.Text
Imports System.Data
Imports System.Linq
Imports System.Collections.Generic
Imports GrapeCity.Documents.Pdf
Imports GrapeCity.Documents.Text
Imports GrapeCity.Documents.Html
Imports System.Globalization
Imports s2industries.ZUGFeRD

'' This sample creates a PDF invoice using the DsNWind sample database,
'' And attaches an XML file to it that Is created according to the ZUGFeRD 1.x standard rules.
''
'' ZUGFeRD Is a German e-invoicing standard based around PDF And XML file formats.
'' Its poised to change the way invoices are handled And can be used by any sort of business.
'' It will make invoice processing more efficient for senders And customers.
'' For details please see What Is ZUGFeRD?.
''
'' This sample uses the ZUGFeRD-csharp package
'' to create the ZUGFeRD-compatible XML that Is attached to the invoice.
''
'' For details on using DsHtml to render HTML to PDF please see HelloWorldHtml.
Public Class ZugferdInvoice
    Public Sub CreatePDF(ByVal stream As Stream)
        Using ds = New DataSet()
            ds.ReadXml(Path.Combine("Resources", "data", "DsNWind.xml"))

            Dim dtSuppliers = ds.Tables("Suppliers")
            Dim dtOrders = ds.Tables("OrdersCustomersEmployees")
            Dim dtOrdersDetails = ds.Tables("EmployeesProductsOrders")
            Dim culture = CultureInfo.CreateSpecificCulture("en-US")

            '' Collect order data:
            Dim random = Util.NewRandom()

            Dim fetchedIndex = random.Next(dtSuppliers.Select().Count())
            Dim supplier =
                dtSuppliers.Select().
                Skip(fetchedIndex).Take(1).
                Select(Function(it) New With {
                    .SupplierID = Convert.ToInt32(it("SupplierID")),
                    .CompanyName = it("CompanyName").ToString(),
                    .ContactName = it("ContactName").ToString(),
                    .ContactTitle = it("ContactTitle").ToString(),
                    .Address = it("Address").ToString(),
                    .City = it("City").ToString(),
                    .Region = it("Region").ToString(),
                    .PostalCode = it("PostalCode").ToString(),
                    .Country = it("Country").ToString(),
                    .Phone = it("Phone").ToString(),
                    .Fax = it("Fax").ToString(),
                    .HomePage = it("HomePage").ToString()
                }).FirstOrDefault()

            fetchedIndex = random.Next(dtOrders.Select().Count())
            Dim order =
                dtOrders.Select().
                Skip(fetchedIndex).Take(1).
                Select(Function(it) New With {
                    .OrderID = Convert.ToInt32(it("OrderID")),
                    .CompanyName = it("CompanyName").ToString(),
                    .LastName = it("LastName").ToString(),
                    .FirstName = it("FirstName").ToString(),
                    .OrderDate = ConvertToDateTime(it("OrderDate")),
                    .RequiredDate = ConvertToDateTime(it("RequiredDate")),
                    .ShippedDate = ConvertToDateTime(it("ShippedDate")),
                    .ShipVia = Convert.ToInt32(it("ShipVia")),
                    .Freight = Convert.ToDecimal(it("Freight")),
                    .ShipName = it("ShipName").ToString(),
                    .ShipAddress = it("ShipAddress").ToString(),
                    .ShipCity = it("ShipCity").ToString(),
                    .ShipRegion = it("ShipRegion").ToString(),
                    .ShipPostalCode = it("ShipPostalCode").ToString(),
                    .ShipCountry = it("ShipCountry").ToString()
                }).FirstOrDefault()

            Dim orderDetails = dtOrdersDetails.Select().
                Select(Function(it) New With {
                    .OrderID = Convert.ToInt32(it("OrderID")),
                    .ItemDescription = it("ProductName").ToString(),
                    .Rate = Convert.ToDecimal(it("UnitPrice")),
                    .Quantity = Convert.ToDecimal(it("Quantity"))
                }).Where(Function(it) it.OrderID = order.OrderID).
                OrderBy(Function(it) it.ItemDescription).ToList()

            Dim orderSubTotal As Decimal = 0
            Dim index = 1
            Dim detailsHtml = New StringBuilder()
            orderDetails.ForEach(
                Sub(it)
                    Dim total = Math.Round(it.Rate * it.Quantity, 2)
                    detailsHtml.AppendFormat(c_dataRowFmt, index,
                    it.ItemDescription,
                    it.Rate.ToString("C", culture),
                    it.Quantity,
                    total.ToString("C", culture))
                    orderSubTotal += total
                    index += 1
                End Sub)
            Dim orderTax As Decimal = Math.Round(orderSubTotal / 4, 2)
            Dim allowanceTotalAmount As Decimal = orderSubTotal
            Dim taxBasisTotalAmount As Decimal = orderSubTotal
            Dim taxTotalAmount As Decimal = orderTax
            Dim grandTotalAmount As Decimal = orderSubTotal

            '' Build HTML to be converted to PDF:
            Dim html = String.Format(
                c_tableTpl, detailsHtml.ToString(),
                supplier.CompanyName,
                $"{supplier.Address}, {supplier.Region} {supplier.PostalCode}, {supplier.City} {supplier.Country}",
                supplier.Phone,
                supplier.HomePage,
                $"{order.FirstName} {order.LastName} {order.CompanyName}",
                $"{order.ShipAddress}, {order.ShipRegion} {order.ShipPostalCode}, {order.ShipCity} {order.ShipCountry}",
                order.OrderDate.ToString("MM/dd/yyyy", culture),
                order.RequiredDate.ToString("MM/dd/yyyy", culture),
                orderSubTotal.ToString("C", culture),
                orderTax.ToString("C", culture),
                (orderSubTotal + orderTax).ToString("C", culture),
                c_tableStyles)

            '' Build ZUGFeRD compliant XML to attach to the PDF:
            Dim zugferdDesc = InvoiceDescriptor.CreateInvoice(
                order.OrderID.ToString(),
                order.OrderDate,
                CurrencyCodes.USD)

            '' Fill the invoice buyer info
            zugferdDesc.SetBuyer(
                order.ShipName,
                order.ShipPostalCode,
                order.ShipCity,
                order.ShipAddress,
                GetCountryCode(order.ShipCountry),
                order.CompanyName)
            zugferdDesc.SetBuyerContact($"{order.FirstName} {order.LastName}")
            '' Fill the invoice seller info:
            zugferdDesc.SetSeller(
                supplier.CompanyName,
                supplier.PostalCode,
                supplier.City,
                supplier.Address,
                GetCountryCode(supplier.Country),
                supplier.CompanyName)
            '' Delivery date & totals:
            zugferdDesc.ActualDeliveryDate = order.RequiredDate
            zugferdDesc.SetTotals(orderSubTotal, orderTax, allowanceTotalAmount, taxBasisTotalAmount, taxTotalAmount, grandTotalAmount)
            '' Payment positions part:
            orderDetails.ForEach(
                Sub(it)
                    zugferdDesc.AddTradeLineItem(name:=it.ItemDescription,
                    billedQuantity:=it.Quantity,
                    netUnitPrice:=it.Rate,
                    unitCode:=QuantityCodes.C62)
                End Sub)

            '' Save the invoice info XML:
            Using ms = New MemoryStream()
                zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic)
                ms.Seek(0, SeekOrigin.Begin)

                Dim tmpPdf = Path.GetTempFileName()

                '' Create an instance of GcHtmlBrowser that Is used to render HTML
                Using browser = Util.NewHtmlBrowser(), htmlPage = browser.NewPage(html)
                    '' Set up HTML headers, margins etc (see HtmlSettings):
                    Dim pdfOptions = New PdfOptions() With
                    {
                        .Margins = New PdfMargins(0.2F, 1, 0.2F, 1),
                        .DisplayHeaderFooter = True,
                        .HeaderTemplate = "<div style='color:#1a5276; font-size:12px; width:1000px; margin-left:0.2in; margin-right:0.2in'>" +
                            "<span style='float:left;'>Invoice</span>" +
                            "<span style='float:right'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></span>" +
                            "</div>",
                        .FooterTemplate = "<div style='color: #1a5276; font-size:12em; width:1000px; margin-left:0.2in; margin-right:0.2in;'>" +
                            "<span>(c) MESCIUS inc. All rights reserved.</span>" +
                            "<span style='float:right'>Generated on <span class='date'></span></span></div>"
                    }
                    '' Render the source Web page to the temporary file:
                    htmlPage.SaveAsPdf(tmpPdf, pdfOptions)
                End Using

                '' Create the result PDF as a PDF/A-3 compliant document with the ZUGFeRD XML attachment:
                Dim doc = New GcPdfDocument()
                Dim tmpZugferd = Path.GetTempFileName()
                Using fs = File.OpenRead(tmpPdf)
                    doc.Load(fs)
                    '' The generated document should contain FileID
                    Dim fid As FileID = doc.FileID
                    If fid Is Nothing Then
                        fid = New FileID()
                        doc.FileID = fid
                    End If
                    If fid.PermanentID Is Nothing Then
                        fid.PermanentID = Guid.NewGuid().ToByteArray()
                    End If
                    If fid.ChangingID Is Nothing Then
                        fid.ChangingID = fid.PermanentID
                    End If
                    Dim ef1 = EmbeddedFileStream.FromBytes(doc, ms.ToArray())
                    ef1.ModificationDate = Util.TimeNow()
                    ef1.MimeType = "text/xml"
                    '' According to the ZUGFeRD 1.x standard naming, the filename should be ZUGFeRD-invoice.xml:
                    Dim fspec = FileSpecification.FromEmbeddedStream("ZUGFeRD-invoice.xml", ef1)
                    fspec.Relationship = AFRelationship.Alternative
                    fspec.UnicodeFile.FileName = fspec.File.FileName
                    '' The embedded file should be associated with a document
                    doc.AssociatedFiles.Add(fspec)
                    '' The attachment dictionary key can be anything:
                    doc.EmbeddedFiles.Add("ZUGfERD-Attachment", fspec)
                    doc.ConformanceLevel = PdfAConformanceLevel.PdfA3a
                    doc.Metadata.PdfA = PdfAConformanceLevel.PdfA3b
                    doc.Metadata.CreatorTool = doc.DocumentInfo.Creator
                    doc.Metadata.Title = "DsPdf Document"
                    doc.Save(tmpZugferd)
                End Using

                '' Copy the created PDF from the temp file to target stream:
                Using ts = File.OpenRead(tmpZugferd)
                    ts.CopyTo(stream)
                End Using

                '' Clean up:
                File.Delete(tmpZugferd)
                File.Delete(tmpPdf)
            End Using
        End Using
        '' Done.
    End Sub

    '' Some records in our sample database lack some dates:
    Private Shared Function ConvertToDateTime(ByVal value As Object) As DateTime
        If (Convert.IsDBNull(value)) Then
            Return DateTime.MinValue
        Else
            Return Convert.ToDateTime(value)
        End If
    End Function

    '' Provide ZUGFeRD country codes:
    Private Shared s_regions As Dictionary(Of String, RegionInfo) = Nothing

    Private Shared Sub InitNames()
        s_regions = New Dictionary(Of String, RegionInfo)()
        For Each culture In CultureInfo.GetCultures(CultureTypes.SpecificCultures)
            If Not s_regions.ContainsKey(culture.Name) Then
                s_regions.Add(culture.Name, New RegionInfo(culture.Name))
            End If
        Next
    End Sub

    Private Shared Function GetCountryCode(ByVal name As String) As CountryCodes
        If s_regions Is Nothing Then
            InitNames()
        End If

        name = name.Trim()

        '' 'UK' is not present in s_regions but is used by our sample database:
        If name.Equals("UK", StringComparison.InvariantCultureIgnoreCase) Then
            name = "United Kingdom"
        End If

        Dim region = s_regions.Values.FirstOrDefault(Function(it) it.EnglishName.Equals(name, StringComparison.InvariantCultureIgnoreCase) OrElse
            it.NativeName.Equals(name, StringComparison.InvariantCultureIgnoreCase) OrElse
            it.ThreeLetterISORegionName.Equals(name, StringComparison.InvariantCultureIgnoreCase))

        If region IsNot Nothing Then
            Return New CountryCodes().FromString(region.Name)
        Else
            Return CountryCodes.Unknown
        End If
    End Function


    '' HTML styles and templates used to render the invoice:
    Const c_tableStyles = "
        <style>
        .clearfix:after {
            display: table;
            clear: both;
        }
        a {
            color: RoyalBlue;
            text-decoration: none;
        }
        body {
            position: relative;
            margin: 0 auto;
            color: #555555;
            background: #FFFFFF;
            font-family: Arial, sans-serif;
            font-size: 14px;
        }
        header {
            padding: 10px 0;
            margin-bottom: 20px;
            min-height: 60px;
            border-bottom: 1px solid #AAAAAA;
        }
        # company {
            float: right;
            text-align: right;
        }
        # details {
            margin-bottom: 50px;
        }
        # client {
            padding-left: 6px;
            border-left: 6px solid RoyalBlue;
            float: left;
        }
        # client .to {
            color: #777777;
        }
        h2.name {
            font-size: 16px;
            font-weight: normal;
            margin: 0;
        }
        # invoice {
            float: right;
            text-align: right;
        }
        # invoice h1 {
            color: RoyalBlue;
            font-size: 18px;
            line-height: 1em;
            font-weight: normal;
            margin: 0  0 10px 0;
        }
        # invoice .date {
            font-size: 14px;
            color: #777777;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            border-spacing: 0;
            margin-bottom: 20px;
        }
        table th {
            padding: 14px;
            color: White !important;
            background: #6585e7 !important;
            text-align: center;
            border-bottom: 1px solid #FFFFFF;
        }
        table td {
            padding: 10px;
            background: #EEEEEE;
            text-align: center;
            border-bottom: 1px solid #FFFFFF;
        }
        table th {
            white-space: nowrap;
            font-weight: normal;
        }
        table td {
            text-align: right;
        }
        table td h3{
            color: RoyalBlue;
            font-size: 14px;
            font-weight: normal;
            margin: 0 0 0.2em 0;
        }
        table .no {
            color: #FFFFFF;
            font-size: 14px;
            background: RoyalBlue;
        }
        table .desc {
            text-align: left;
        }
        table .unit {
            background: #DDDDDD;
        }
        table .qty {
        }
        table .total {
            background: RoyalBlue;
            color: #FFFFFF;
        }
        table td.unit,
        table td.qty,
        table td.total {
            font-size: 14px;
        }
        table tbody tr:last-child td {
            border: none;
        }
        table tfoot td {
            padding: 10px 20px;
            background: #FFFFFF;
            border-bottom: none;
            font-size: 16px;
            white-space: nowrap;
            border-top: 1px solid #AAAAAA;
        }
        table tfoot tr:first-child td {
            border-top: none;
        }
        table tfoot tr:last-child td {
            color: RoyalBlue;
            font-size: 16px;
            border-top: 1px solid RoyalBlue;
        }
        table tfoot tr td:first-child {
            border: none;
        }
        # thanks{
            font-size: 16px;
            margin-bottom: 50px;
        }
        # notes{
            padding-left: 6px;
            border-left: 6px solid RoyalBlue;
        }
        # notes .note {
            font-size: 16px;
        }
        footer {
            color: #777777;
            width: 100%;
            height: 30px;
            position: absolute;
            bottom: 0;
            border-top: 1px solid #AAAAAA;
            padding: 8px 0;
            text-align: center;
        }
        </style>
    "

    Const c_tableTpl = "
        <!DOCTYPE html>
        <html lang='en'>
        <head><meta charset='utf-8'>{12}</head>
        <body>
            <header class='clearfix'>
            <div id = 'company'>
                <h2 class='name'>{1}</h2>
                <div>{2}</div>
                <div>{3}</div>
                <div><a href = '{4}'> {4}</a></div>
            </div>
            </header>
            <main>
            <div id='details' class='clearfix'>
                <div id='client'>
                <div class='to'>INVOICE TO:</div>
                <h2 class='name'>{5}</h2>
                <div class='address'>{6}</div>
                </div>
                <div id='invoice'>
                <h1>INVOICE</h1>
                <div class='date'>Date of Invoice: {7}</div>
                <div class='date'>Due Date: {8}</div>
                </div>
            </div>
            <table border='0' cellspacing='0' cellpadding='0'>
                <thead>
                <tr>
                    <th class='no'>#</th>
                    <th class='desc'>DESCRIPTION</th>
                    <th class='unit'>UNIT PRICE</th>
                    <th class='qty'>QUANTITY</th>
                    <th class='total'>TOTAL</th>
                </tr>
                </thead>
                <tbody>
                {0}
                </tbody>
                <tfoot>
                <tr>
                    <td colspan='2'></td>
                    <td colspan='2'>SUBTOTAL</td>
                    <td>{9}</td>
                </tr>
                <tr>
                    <td colspan='2'></td>
                    <td colspan='2'>TAX 25%</td>
                    <td>{10}</td>
                </tr>
                <tr>
                    <td colspan='2'></td>
                    <td colspan='2'>GRAND TOTAL</td>
                    <td>{11}</td>
                </tr>
                </tfoot>
            </table>
            <div id='thanks'>Thank you!</div>
            <div id='notes'>
                <div>NOTES:</div>
                <div class='note'></div>
            </div>
            </main>
        </body>
        </html>
    "
    Const c_dataRowFmt = "
        <tr>
            <td class='no'>{0}</td>
            <td class='desc'><h3>{1}</h3></td>
            <td class='unit'>{2}</td>
            <td class='qty'>{3}</td>
            <td class='total'>{4}</td>
        </tr>
    "
End Class