ZugferdInvoice.vb
'' 完毕:
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

'' 此示例使用 DsNWind 示例数据库创建 PDF 发票,
'' 并将根据 ZUGFeRD 1.x 标准规则创建的 XML 文件附加到其中。
'' 
'' ZUGFeRD 是基于 PDF 和 XML 文件格式的德国电子发票标准。
'' 它准备改变发票的处理方式,并可供任何类型的企业使用。
'' 它将使发件人和客户的发票处理更加高效。
'' 详细信息请参见什么是ZUGFeRD? 。
'' 
'' 此示例​​使用 ZUGFeRD-csharp 包
'' 创建附加到发票的 ZUGFeRD 兼容 XML。
'' 
'' 有关使用 GcDocs.Html 将 HTML 渲染为 PDF 的详细信息,请参阅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")

            '' 收集订单数据:
            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

            '' 构建要转换为 PDF 的 HTML:
            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)

            '' 构建 ZUGFeRD 兼容的 XML 以附加到 PDF:
            Dim zugferdDesc = InvoiceDescriptor.CreateInvoice(
                order.OrderID.ToString(),
                order.OrderDate,
                CurrencyCodes.USD)

            '' 填写发票买家信息:
            zugferdDesc.SetBuyer(
                order.ShipName,
                order.ShipPostalCode,
                order.ShipCity,
                order.ShipAddress,
                GetCountryCode(order.ShipCountry),
                order.CompanyName)
            zugferdDesc.SetBuyerContact($"{order.FirstName} {order.LastName}")
            '' 填写发票卖家信息:
            zugferdDesc.SetSeller(
                supplier.CompanyName,
                supplier.PostalCode,
                supplier.City,
                supplier.Address,
                GetCountryCode(supplier.Country),
                supplier.CompanyName)
            '' 交货日期和总计:
            zugferdDesc.ActualDeliveryDate = order.RequiredDate
            zugferdDesc.SetTotals(orderSubTotal, orderTax, allowanceTotalAmount, taxBasisTotalAmount, taxTotalAmount, grandTotalAmount)
            '' 付款仓位部分:
            orderDetails.ForEach(
                Sub(it)
                    zugferdDesc.AddTradeLineItem(name:=it.ItemDescription,
                    billedQuantity:=it.Quantity,
                    netUnitPrice:=it.Rate,
                    unitCode:=QuantityCodes.C62)
                End Sub)

            '' 保存发票信息 XML:
            Using ms = New MemoryStream()
                zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic)
                ms.Seek(0, SeekOrigin.Begin)

                Dim tmpPdf = Path.GetTempFileName()

                '' 创建一个用于呈现 HTML 的 GcHtmlBrowser 实例:
                Using browser = Util.NewHtmlBrowser(), htmlPage = browser.NewPage(html)
                    '' 设置 HTML 标题、边距等(参见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>"
                    }
                    '' 将源网页渲染到临时文件:
                    htmlPage.SaveAsPdf(tmpPdf, pdfOptions)
                End Using

                '' 使用 ZUGFeRD XML 附件将结果 PDF 创建为 PDF/A-3 兼容文档:
                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"
                    '' 根据 ZUGFeRD 1.x 标准命名,文件名应为 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)
                    '' 附件字典键可以是任何内容:
                    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

                '' 将创建的 PDF 从临时文件复制到目标流:
                Using ts = File.OpenRead(tmpZugferd)
                    ts.CopyTo(stream)
                End Using

                '' 清理:
                File.Delete(tmpZugferd)
                File.Delete(tmpPdf)
            End Using
        End Using
        '' 完毕:
    End Sub

    '' 我们的示例数据库中的一些记录缺少一些日期:
    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

    '' 提供 ZUGFeRD 国家代码:
    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”不存在于 s_regions 中,但被我们的示例数据库使用:
        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 样式和模板:
    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