ZugferdInvoice.cs
// 完毕:
using System;
using System.IO;
using System.Drawing;
using System.Text;
using System.Data;
using System.Linq;
using System.Collections.Generic;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Html;
using System.Globalization;
using s2industries.ZUGFeRD;

namespace DsPdfWeb.Demos
{
    // 此示例使用 DsNWind 示例数据库创建 PDF 发票,
    // 并将根据 ZUGFeRD 1.x 标准规则创建的 XML 文件附加到其中。
    // 
    // ZUGFeRD 是基于 PDF 和 XML 文件格式的德国电子发票标准。
    // 它准备改变发票的处理方式,并可供任何类型的企业使用。
    // 它将使发件人和客户的发票处理更加高效。
    // 详细信息请参见什么是ZUGFeRD? 。
    // 
    // 此示例​​使用 ZUGFeRD-csharp 包
    // 创建附加到发票的 ZUGFeRD 兼容 XML。
    // 
    // 有关使用 GcDocs.Html 将 HTML 渲染为 PDF 的详细信息,请参阅HelloWorldHtml。
    public class ZugferdInvoice
    {
        public void CreatePDF(Stream stream)
        {
            using var ds = new DataSet();

            ds.ReadXml(Path.Combine("Resources", "data", "DsNWind.xml"));

            var dtSuppliers = ds.Tables["Suppliers"];
            var dtOrders = ds.Tables["OrdersCustomersEmployees"];
            var dtOrdersDetails = ds.Tables["EmployeesProductsOrders"];
            var culture = CultureInfo.CreateSpecificCulture("en-US");

            // 收集订单数据:
            var random = Common.Util.NewRandom();

            var fetchedIndex = random.Next(dtSuppliers.Select().Count());
            var supplier =
                dtSuppliers.Select()
                .Skip(fetchedIndex).Take(1)
                .Select(it => new
                {
                    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());
            var order = dtOrders.Select()
                .Skip(fetchedIndex).Take(1)
                .Select(it => new
                {
                    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();

            var orderDetails = dtOrdersDetails.Select()
                .Select(it => new
                {
                    OrderID = Convert.ToInt32(it["OrderID"]),
                    ItemDescription = it["ProductName"].ToString(),
                    Rate = Convert.ToDecimal(it["UnitPrice"]),
                    Quantity = Convert.ToDecimal(it["Quantity"])
                })
                .Where(it => it.OrderID == order.OrderID)
                .OrderBy(it => it.ItemDescription).ToList();

            decimal orderSubTotal = 0;
            var index = 1;
            var detailsHtml = new StringBuilder();
            orderDetails.ForEach(it =>
            {
                var 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++;
            });
            decimal orderTax = Math.Round(orderSubTotal / 4, 2);
            decimal allowanceTotalAmount = orderSubTotal;
            decimal taxBasisTotalAmount = orderSubTotal;
            decimal taxTotalAmount = orderTax;
            decimal grandTotalAmount = orderSubTotal;

            // 构建要转换为 PDF 的 HTML:
            var 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:
            var 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(it =>
            {
                zugferdDesc.AddTradeLineItem(
                    name: it.ItemDescription,
                    billedQuantity: it.Quantity,
                    netUnitPrice: it.Rate,
                    unitCode: QuantityCodes.C62);
            });

            // 保存发票信息 XML:
            using var ms = new MemoryStream();

            zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic);
            ms.Seek(0, SeekOrigin.Begin);

            var tmpPdf = Path.GetTempFileName();
            // 创建一个用于呈现 HTML 的 GcHtmlBrowser 实例:
            using var browser = Common.Util.NewHtmlBrowser();
            using var htmlPage = browser.NewPage(html);

            // 设置 HTML 标题、边距等(参见HtmlSettings):
            var pdfOptions = new PdfOptions()
            {
                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);

            // 使用 ZUGFeRD XML 附件将结果 PDF 创建为 PDF/A-3 兼容文档:
            var doc = new GcPdfDocument();
            var tmpZugferd = Path.GetTempFileName();
            using (var fs = File.OpenRead(tmpPdf))
            {
                doc.Load(fs);
                // The generated document should contain FileID:
                FileID fid = doc.FileID;
                if (fid == null)
                {
                    fid = new FileID();
                    doc.FileID = fid;
                }
                if (fid.PermanentID == null)
                    fid.PermanentID = Guid.NewGuid().ToByteArray();
                if (fid.ChangingID == null)
                    fid.ChangingID = fid.PermanentID;

                var ef1 = EmbeddedFileStream.FromBytes(doc, ms.ToArray());
                ef1.ModificationDate = Common.Util.TimeNow();
                ef1.MimeType = "text/xml";
                // 根据 ZUGFeRD 1.x 标准命名,文件名应为 ZUGFeRD-invoice.xml:
                var 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.PdfA3b;
                doc.Metadata.PdfA = PdfAConformanceLevel.PdfA3b;
                doc.Metadata.CreatorTool = doc.DocumentInfo.Creator;
                doc.Metadata.Title = "DsPdf Document";
                doc.Save(tmpZugferd);
            }

            // 将创建的 PDF 从临时文件复制到目标流:
            using (var ts = File.OpenRead(tmpZugferd))
                ts.CopyTo(stream);

            // 清理:
            File.Delete(tmpZugferd);
            File.Delete(tmpPdf);
            // 完毕:
        }

        // 我们的示例数据库中的一些记录缺少一些日期:
        private static DateTime ConvertToDateTime(object value)
        {
            if (Convert.IsDBNull(value))
                return DateTime.MinValue;
            else
                return Convert.ToDateTime(value);
        }

        // 提供 ZUGFeRD 国家代码:
        private static Dictionary<string, RegionInfo> s_regions = null;

        private static void InitNames()
        {
            s_regions = new Dictionary<string, RegionInfo>();
            foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
            {
                if (!s_regions.ContainsKey(culture.Name))
                    s_regions.Add(culture.Name, new RegionInfo(culture.Name));
            }
        }

        private static CountryCodes GetCountryCode(string name)
        {
            if (s_regions == null)
                InitNames();

            name = name.Trim();

            // “UK”不存在于 s_regions 中,但被我们的示例数据库使用:
            if (name.Equals("UK", StringComparison.InvariantCultureIgnoreCase))
                name = "United Kingdom";

            var region = s_regions.Values.FirstOrDefault(it =>
                it.EnglishName.Equals(name, StringComparison.InvariantCultureIgnoreCase) ||
                it.NativeName.Equals(name, StringComparison.InvariantCultureIgnoreCase) ||
                it.ThreeLetterISORegionName.Equals(name, StringComparison.InvariantCultureIgnoreCase));
            if (region != null)
                return new CountryCodes().FromString(region.Name);
            else
                return CountryCodes.Unknown;
        }

        // 用于呈现发票的 HTML 样式和模板:
        const string 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 string 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 string 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>
        ";
    }
}