ZugferdInvoice.vb
'''' This code is part of Document Solutions for PDF .NET demos.'' Copyright (c) MESCIUS inc. All rights reserved.''Imports System.IOImports System.TextImports System.DataImports System.LinqImports System.Collections.GenericImports GrapeCity.Documents.PdfImports GrapeCity.Documents.HtmlImports System.GlobalizationImports s2industries.ZUGFeRD
'' 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 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")
'' Collect order data: 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
'' Build HTML to be converted to PDF: 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)
'' Build ZUGFeRD compliant XML to attach to the PDF: Dim 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( Sub(it) zugferdDesc.AddTradeLineItem(name:=it.ItemDescription, billedQuantity:=it.Quantity, netUnitPrice:=it.Rate, unitCode:=QuantityCodes.C62) End Sub)
'' Save the invoice info XML: Using ms = New MemoryStream() zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic) ms.Seek(0, SeekOrigin.Begin)
Dim tmpPdf = Path.GetTempFileName()
'' Create an instance of GcHtmlBrowser that Is used to render HTML Using browser = Util.NewHtmlBrowser(), htmlPage = browser.NewPage(html) '' Set up HTML headers, margins etc (see 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>" } '' Render the source Web page to the temporary file: htmlPage.SaveAsPdf(tmpPdf, pdfOptions) End Using
'' Create the result PDF as a PDF/A-3 compliant document with the ZUGFeRD XML attachment: 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" '' According to the ZUGFeRD 1.x standard naming, the filename should be 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) '' The attachment dictionary key can be anything: 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
'' Copy the created PDF from the temp file to target stream: Using ts = File.OpenRead(tmpZugferd) ts.CopyTo(stream) End Using
'' Clean up: File.Delete(tmpZugferd) File.Delete(tmpPdf) End Using End Using '' Done. End Sub
'' Some records in our sample database lack some dates: 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
'' Provide ZUGFeRD country codes: 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' is not present in s_regions but is used by our sample database: 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 styles and templates used to render the invoice: 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