ZugferdInvoice.vb
- ''
- '' This code is part of Document Solutions for PDF demos.
- '' Copyright (c) MESCIUS inc. All rights reserved.
- ''
- Imports System.IO
- Imports System.Drawing
- Imports System.Text
- Imports System.Data
- Imports System.Linq
- Imports System.Collections.Generic
- Imports GrapeCity.Documents.Pdf
- Imports GrapeCity.Documents.Text
- Imports GrapeCity.Documents.Html
- Imports System.Globalization
- Imports 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 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 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
-