ZugferdInvoice.cs
C# VB
//// This code is part of Document Solutions for PDF .NET demos.// Copyright (c) MESCIUS inc. All rights reserved.//using System;using System.IO;using System.Text;using System.Data;using System.Linq;using System.Collections.Generic;using GrapeCity.Documents.Pdf;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 www.ferd-net.de. // // 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> "; }}