ZugferdInvoice.cs
//
// This code is part of GrapeCity Documents for PDF samples.
// Copyright (c) GrapeCity, Inc. All rights reserved.
//
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 GcPdfWeb.Samples
{
    // This sample creates a PDF invoice using the GcNWind 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 GcHtml to render HTML to PDF please see HelloWorldHtml.
    public class ZugferdInvoice
    {
        public void CreatePDF(Stream stream)
        {
            using (var ds = new DataSet())
            {
                ds.ReadXml(Path.Combine("Resources", "data", "GcNWind.xml"));

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

                // Collect order data:
                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);

                // Build HTML to be converted to PDF:
                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"),
                    order.RequiredDate.ToString("MM/dd/yyyy"),
                    orderSubTotal.ToString("C", culture),
                    orderTax.ToString("C", culture),
                    (orderSubTotal + orderTax).ToString("C", culture),
                    c_tableStyles);

                // Build ZUGFeRD compliant XML to attach to the PDF:
                var 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);
                // Payment positions part:
                orderDetails.ForEach(it =>
                {
                    zugferdDesc.addTradeLineItem(
                        name: it.ItemDescription,
                        billedQuantity: it.Quantity,
                        netUnitPrice: it.Rate);
                });

                // Save the invoice info XML:
                using (var ms = new MemoryStream())
                {
                    zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic);
                    ms.Seek(0, SeekOrigin.Begin);

                    var tmpPdf = Path.GetTempFileName();
                    using (var re = new GcHtmlRenderer(html))
                    {
                        // Set up HTML headers, margins etc (see HtmlSettings):
                        var pdfSettings = new PdfSettings()
                        {
                            Margins = new Margins(0.2f, 1, 0.2f, 1),
                            IgnoreCSSPageSize = true,
                            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) GrapeCity, 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:
                        re.RenderToPdf(tmpPdf, pdfSettings);
                    }

                    // Create the result PDF as a PDF/A-3 compliant document with the ZUGFeRD XML attachment:
                    var doc = new GcPdfDocument();
                    var tmpZugferd = Path.GetTempFileName();
                    using (var fs = File.OpenRead(tmpPdf))
                    {
                        doc.Load(fs);
                        doc.ConformanceLevel = PdfAConformanceLevel.PdfA3a;
                        var ef1 = EmbeddedFileStream.FromBytes(doc, ms.ToArray());
                        ef1.ModificationDate = DateTime.Now;
                        ef1.MimeType = "text/xml";
                        // According to the ZUGFeRD 1.x standard naming, the filename should be ZUGFeRD-invoice.xml:
                        var fspec = FileSpecification.FromEmbeddedStream("ZUGFeRD-invoice.xml", ef1);
                        fspec.Relationship = AFRelationship.Alternative;
                        fspec.UnicodeFile.FileName = fspec.File.FileName;
                        // The attachment dictionary key can be anything:
                        doc.EmbeddedFiles.Add("ZUGfERD-Attachment", fspec);
                        doc.Save(tmpZugferd);
                    }

                    // Copy the created PDF from the temp file to target stream:
                    using (var ts = File.OpenRead(tmpZugferd))
                        ts.CopyTo(stream);

                    // Clean up:
                    File.Delete(tmpZugferd);
                    File.Delete(tmpPdf);
                }
            }
            // Done.
        }

        // Some records in our sample database lack some dates:
        private static DateTime ConvertToDateTime(object value)
        {
            if (Convert.IsDBNull(value))
                return DateTime.MinValue;
            else
                return Convert.ToDateTime(value);
        }

        // Provide ZUGFeRD country codes:
        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' is not present in s_regions but is used by our sample database:
            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 styles and templates used to render the invoice:
        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>
        ";
    }
}