ZugferdInvoice.cs
- //
- // This code is part of Document Solutions for PDF demos.
- // Copyright (c) MESCIUS 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 DsPdfWeb.Demos
- {
- // 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 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");
-
- // 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);
- decimal allowanceTotalAmount = orderSubTotal;
- decimal taxBasisTotalAmount = orderSubTotal;
- decimal taxTotalAmount = orderTax;
- decimal grandTotalAmount = orderSubTotal;
-
- // 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", 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:
- 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, allowanceTotalAmount, taxBasisTotalAmount, taxTotalAmount, grandTotalAmount);
-
- // Payment positions part:
- orderDetails.ForEach(it =>
- {
- zugferdDesc.AddTradeLineItem(
- name: it.ItemDescription,
- billedQuantity: it.Quantity,
- netUnitPrice: it.Rate,
- unitCode: QuantityCodes.C62);
- });
-
- // 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();
- // Create an instance of GcHtmlBrowser that is used to render HTML:
- using var browser = Common.Util.NewHtmlBrowser();
- using var htmlPage = browser.NewPage(html);
-
- // Set up HTML headers, margins etc (see 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>"
- };
- // Render the source Web page to the temporary file:
- htmlPage.SaveAsPdf(tmpPdf, pdfOptions);
-
- // 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);
- // 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";
- // 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 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);
- }
-
- // 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>
- ";
- }
- }
-