ZugferdInvoice.vb
  1. ''
  2. '' This code is part of Document Solutions for PDF demos.
  3. '' Copyright (c) MESCIUS inc. All rights reserved.
  4. ''
  5. Imports System.IO
  6. Imports System.Drawing
  7. Imports System.Text
  8. Imports System.Data
  9. Imports System.Linq
  10. Imports System.Collections.Generic
  11. Imports GrapeCity.Documents.Pdf
  12. Imports GrapeCity.Documents.Text
  13. Imports GrapeCity.Documents.Html
  14. Imports System.Globalization
  15. Imports s2industries.ZUGFeRD
  16.  
  17. '' This sample creates a PDF invoice using the DsNWind sample database,
  18. '' And attaches an XML file to it that Is created according to the ZUGFeRD 1.x standard rules.
  19. ''
  20. '' ZUGFeRD Is a German e-invoicing standard based around PDF And XML file formats.
  21. '' Its poised to change the way invoices are handled And can be used by any sort of business.
  22. '' It will make invoice processing more efficient for senders And customers.
  23. '' For details please see What Is ZUGFeRD?.
  24. ''
  25. '' This sample uses the ZUGFeRD-csharp package
  26. '' to create the ZUGFeRD-compatible XML that Is attached to the invoice.
  27. ''
  28. '' For details on using DsHtml to render HTML to PDF please see HelloWorldHtml.
  29. Public Class ZugferdInvoice
  30. Public Sub CreatePDF(ByVal stream As Stream)
  31. Using ds = New DataSet()
  32. ds.ReadXml(Path.Combine("Resources", "data", "DsNWind.xml"))
  33.  
  34. Dim dtSuppliers = ds.Tables("Suppliers")
  35. Dim dtOrders = ds.Tables("OrdersCustomersEmployees")
  36. Dim dtOrdersDetails = ds.Tables("EmployeesProductsOrders")
  37. Dim culture = CultureInfo.CreateSpecificCulture("en-US")
  38.  
  39. '' Collect order data:
  40. Dim random = Util.NewRandom()
  41.  
  42. Dim fetchedIndex = random.Next(dtSuppliers.Select().Count())
  43. Dim supplier =
  44. dtSuppliers.Select().
  45. Skip(fetchedIndex).Take(1).
  46. Select(Function(it) New With {
  47. .SupplierID = Convert.ToInt32(it("SupplierID")),
  48. .CompanyName = it("CompanyName").ToString(),
  49. .ContactName = it("ContactName").ToString(),
  50. .ContactTitle = it("ContactTitle").ToString(),
  51. .Address = it("Address").ToString(),
  52. .City = it("City").ToString(),
  53. .Region = it("Region").ToString(),
  54. .PostalCode = it("PostalCode").ToString(),
  55. .Country = it("Country").ToString(),
  56. .Phone = it("Phone").ToString(),
  57. .Fax = it("Fax").ToString(),
  58. .HomePage = it("HomePage").ToString()
  59. }).FirstOrDefault()
  60.  
  61. fetchedIndex = random.Next(dtOrders.Select().Count())
  62. Dim order =
  63. dtOrders.Select().
  64. Skip(fetchedIndex).Take(1).
  65. Select(Function(it) New With {
  66. .OrderID = Convert.ToInt32(it("OrderID")),
  67. .CompanyName = it("CompanyName").ToString(),
  68. .LastName = it("LastName").ToString(),
  69. .FirstName = it("FirstName").ToString(),
  70. .OrderDate = ConvertToDateTime(it("OrderDate")),
  71. .RequiredDate = ConvertToDateTime(it("RequiredDate")),
  72. .ShippedDate = ConvertToDateTime(it("ShippedDate")),
  73. .ShipVia = Convert.ToInt32(it("ShipVia")),
  74. .Freight = Convert.ToDecimal(it("Freight")),
  75. .ShipName = it("ShipName").ToString(),
  76. .ShipAddress = it("ShipAddress").ToString(),
  77. .ShipCity = it("ShipCity").ToString(),
  78. .ShipRegion = it("ShipRegion").ToString(),
  79. .ShipPostalCode = it("ShipPostalCode").ToString(),
  80. .ShipCountry = it("ShipCountry").ToString()
  81. }).FirstOrDefault()
  82.  
  83. Dim orderDetails = dtOrdersDetails.Select().
  84. Select(Function(it) New With {
  85. .OrderID = Convert.ToInt32(it("OrderID")),
  86. .ItemDescription = it("ProductName").ToString(),
  87. .Rate = Convert.ToDecimal(it("UnitPrice")),
  88. .Quantity = Convert.ToDecimal(it("Quantity"))
  89. }).Where(Function(it) it.OrderID = order.OrderID).
  90. OrderBy(Function(it) it.ItemDescription).ToList()
  91.  
  92. Dim orderSubTotal As Decimal = 0
  93. Dim index = 1
  94. Dim detailsHtml = New StringBuilder()
  95. orderDetails.ForEach(
  96. Sub(it)
  97. Dim total = Math.Round(it.Rate * it.Quantity, 2)
  98. detailsHtml.AppendFormat(c_dataRowFmt, index,
  99. it.ItemDescription,
  100. it.Rate.ToString("C", culture),
  101. it.Quantity,
  102. total.ToString("C", culture))
  103. orderSubTotal += total
  104. index += 1
  105. End Sub)
  106. Dim orderTax As Decimal = Math.Round(orderSubTotal / 4, 2)
  107. Dim allowanceTotalAmount As Decimal = orderSubTotal
  108. Dim taxBasisTotalAmount As Decimal = orderSubTotal
  109. Dim taxTotalAmount As Decimal = orderTax
  110. Dim grandTotalAmount As Decimal = orderSubTotal
  111.  
  112. '' Build HTML to be converted to PDF:
  113. Dim html = String.Format(
  114. c_tableTpl, detailsHtml.ToString(),
  115. supplier.CompanyName,
  116. $"{supplier.Address}, {supplier.Region} {supplier.PostalCode}, {supplier.City} {supplier.Country}",
  117. supplier.Phone,
  118. supplier.HomePage,
  119. $"{order.FirstName} {order.LastName} {order.CompanyName}",
  120. $"{order.ShipAddress}, {order.ShipRegion} {order.ShipPostalCode}, {order.ShipCity} {order.ShipCountry}",
  121. order.OrderDate.ToString("MM/dd/yyyy", culture),
  122. order.RequiredDate.ToString("MM/dd/yyyy", culture),
  123. orderSubTotal.ToString("C", culture),
  124. orderTax.ToString("C", culture),
  125. (orderSubTotal + orderTax).ToString("C", culture),
  126. c_tableStyles)
  127.  
  128. '' Build ZUGFeRD compliant XML to attach to the PDF:
  129. Dim zugferdDesc = InvoiceDescriptor.CreateInvoice(
  130. order.OrderID.ToString(),
  131. order.OrderDate,
  132. CurrencyCodes.USD)
  133.  
  134. '' Fill the invoice buyer info
  135. zugferdDesc.SetBuyer(
  136. order.ShipName,
  137. order.ShipPostalCode,
  138. order.ShipCity,
  139. order.ShipAddress,
  140. GetCountryCode(order.ShipCountry),
  141. order.CompanyName)
  142. zugferdDesc.SetBuyerContact($"{order.FirstName} {order.LastName}")
  143. '' Fill the invoice seller info:
  144. zugferdDesc.SetSeller(
  145. supplier.CompanyName,
  146. supplier.PostalCode,
  147. supplier.City,
  148. supplier.Address,
  149. GetCountryCode(supplier.Country),
  150. supplier.CompanyName)
  151. '' Delivery date & totals:
  152. zugferdDesc.ActualDeliveryDate = order.RequiredDate
  153. zugferdDesc.SetTotals(orderSubTotal, orderTax, allowanceTotalAmount, taxBasisTotalAmount, taxTotalAmount, grandTotalAmount)
  154. '' Payment positions part:
  155. orderDetails.ForEach(
  156. Sub(it)
  157. zugferdDesc.AddTradeLineItem(name:=it.ItemDescription,
  158. billedQuantity:=it.Quantity,
  159. netUnitPrice:=it.Rate,
  160. unitCode:=QuantityCodes.C62)
  161. End Sub)
  162.  
  163. '' Save the invoice info XML:
  164. Using ms = New MemoryStream()
  165. zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic)
  166. ms.Seek(0, SeekOrigin.Begin)
  167.  
  168. Dim tmpPdf = Path.GetTempFileName()
  169.  
  170. '' Create an instance of GcHtmlBrowser that Is used to render HTML
  171. Using browser = Util.NewHtmlBrowser(), htmlPage = browser.NewPage(html)
  172. '' Set up HTML headers, margins etc (see HtmlSettings):
  173. Dim pdfOptions = New PdfOptions() With
  174. {
  175. .Margins = New PdfMargins(0.2F, 1, 0.2F, 1),
  176. .DisplayHeaderFooter = True,
  177. .HeaderTemplate = "<div style='color:#1a5276; font-size:12px; width:1000px; margin-left:0.2in; margin-right:0.2in'>" +
  178. "<span style='float:left;'>Invoice</span>" +
  179. "<span style='float:right'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></span>" +
  180. "</div>",
  181. .FooterTemplate = "<div style='color: #1a5276; font-size:12em; width:1000px; margin-left:0.2in; margin-right:0.2in;'>" +
  182. "<span>(c) MESCIUS inc. All rights reserved.</span>" +
  183. "<span style='float:right'>Generated on <span class='date'></span></span></div>"
  184. }
  185. '' Render the source Web page to the temporary file:
  186. htmlPage.SaveAsPdf(tmpPdf, pdfOptions)
  187. End Using
  188.  
  189. '' Create the result PDF as a PDF/A-3 compliant document with the ZUGFeRD XML attachment:
  190. Dim doc = New GcPdfDocument()
  191. Dim tmpZugferd = Path.GetTempFileName()
  192. Using fs = File.OpenRead(tmpPdf)
  193. doc.Load(fs)
  194. '' The generated document should contain FileID
  195. Dim fid As FileID = doc.FileID
  196. If fid Is Nothing Then
  197. fid = New FileID()
  198. doc.FileID = fid
  199. End If
  200. If fid.PermanentID Is Nothing Then
  201. fid.PermanentID = Guid.NewGuid().ToByteArray()
  202. End If
  203. If fid.ChangingID Is Nothing Then
  204. fid.ChangingID = fid.PermanentID
  205. End If
  206. Dim ef1 = EmbeddedFileStream.FromBytes(doc, ms.ToArray())
  207. ef1.ModificationDate = Util.TimeNow()
  208. ef1.MimeType = "text/xml"
  209. '' According to the ZUGFeRD 1.x standard naming, the filename should be ZUGFeRD-invoice.xml:
  210. Dim fspec = FileSpecification.FromEmbeddedStream("ZUGFeRD-invoice.xml", ef1)
  211. fspec.Relationship = AFRelationship.Alternative
  212. fspec.UnicodeFile.FileName = fspec.File.FileName
  213. '' The embedded file should be associated with a document
  214. doc.AssociatedFiles.Add(fspec)
  215. '' The attachment dictionary key can be anything:
  216. doc.EmbeddedFiles.Add("ZUGfERD-Attachment", fspec)
  217. doc.ConformanceLevel = PdfAConformanceLevel.PdfA3a
  218. doc.Metadata.PdfA = PdfAConformanceLevel.PdfA3b
  219. doc.Metadata.CreatorTool = doc.DocumentInfo.Creator
  220. doc.Metadata.Title = "DsPdf Document"
  221. doc.Save(tmpZugferd)
  222. End Using
  223.  
  224. '' Copy the created PDF from the temp file to target stream:
  225. Using ts = File.OpenRead(tmpZugferd)
  226. ts.CopyTo(stream)
  227. End Using
  228.  
  229. '' Clean up:
  230. File.Delete(tmpZugferd)
  231. File.Delete(tmpPdf)
  232. End Using
  233. End Using
  234. '' Done.
  235. End Sub
  236.  
  237. '' Some records in our sample database lack some dates:
  238. Private Shared Function ConvertToDateTime(ByVal value As Object) As DateTime
  239. If (Convert.IsDBNull(value)) Then
  240. Return DateTime.MinValue
  241. Else
  242. Return Convert.ToDateTime(value)
  243. End If
  244. End Function
  245.  
  246. '' Provide ZUGFeRD country codes:
  247. Private Shared s_regions As Dictionary(Of String, RegionInfo) = Nothing
  248.  
  249. Private Shared Sub InitNames()
  250. s_regions = New Dictionary(Of String, RegionInfo)()
  251. For Each culture In CultureInfo.GetCultures(CultureTypes.SpecificCultures)
  252. If Not s_regions.ContainsKey(culture.Name) Then
  253. s_regions.Add(culture.Name, New RegionInfo(culture.Name))
  254. End If
  255. Next
  256. End Sub
  257.  
  258. Private Shared Function GetCountryCode(ByVal name As String) As CountryCodes
  259. If s_regions Is Nothing Then
  260. InitNames()
  261. End If
  262.  
  263. name = name.Trim()
  264.  
  265. '' 'UK' is not present in s_regions but is used by our sample database:
  266. If name.Equals("UK", StringComparison.InvariantCultureIgnoreCase) Then
  267. name = "United Kingdom"
  268. End If
  269.  
  270. Dim region = s_regions.Values.FirstOrDefault(Function(it) it.EnglishName.Equals(name, StringComparison.InvariantCultureIgnoreCase) OrElse
  271. it.NativeName.Equals(name, StringComparison.InvariantCultureIgnoreCase) OrElse
  272. it.ThreeLetterISORegionName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
  273.  
  274. If region IsNot Nothing Then
  275. Return New CountryCodes().FromString(region.Name)
  276. Else
  277. Return CountryCodes.Unknown
  278. End If
  279. End Function
  280.  
  281.  
  282. '' HTML styles and templates used to render the invoice:
  283. Const c_tableStyles = "
  284. <style>
  285. .clearfix:after {
  286. display: table;
  287. clear: both;
  288. }
  289. a {
  290. color: RoyalBlue;
  291. text-decoration: none;
  292. }
  293. body {
  294. position: relative;
  295. margin: 0 auto;
  296. color: #555555;
  297. background: #FFFFFF;
  298. font-family: Arial, sans-serif;
  299. font-size: 14px;
  300. }
  301. header {
  302. padding: 10px 0;
  303. margin-bottom: 20px;
  304. min-height: 60px;
  305. border-bottom: 1px solid #AAAAAA;
  306. }
  307. # company {
  308. float: right;
  309. text-align: right;
  310. }
  311. # details {
  312. margin-bottom: 50px;
  313. }
  314. # client {
  315. padding-left: 6px;
  316. border-left: 6px solid RoyalBlue;
  317. float: left;
  318. }
  319. # client .to {
  320. color: #777777;
  321. }
  322. h2.name {
  323. font-size: 16px;
  324. font-weight: normal;
  325. margin: 0;
  326. }
  327. # invoice {
  328. float: right;
  329. text-align: right;
  330. }
  331. # invoice h1 {
  332. color: RoyalBlue;
  333. font-size: 18px;
  334. line-height: 1em;
  335. font-weight: normal;
  336. margin: 0 0 10px 0;
  337. }
  338. # invoice .date {
  339. font-size: 14px;
  340. color: #777777;
  341. }
  342. table {
  343. width: 100%;
  344. border-collapse: collapse;
  345. border-spacing: 0;
  346. margin-bottom: 20px;
  347. }
  348. table th {
  349. padding: 14px;
  350. color: White !important;
  351. background: #6585e7 !important;
  352. text-align: center;
  353. border-bottom: 1px solid #FFFFFF;
  354. }
  355. table td {
  356. padding: 10px;
  357. background: #EEEEEE;
  358. text-align: center;
  359. border-bottom: 1px solid #FFFFFF;
  360. }
  361. table th {
  362. white-space: nowrap;
  363. font-weight: normal;
  364. }
  365. table td {
  366. text-align: right;
  367. }
  368. table td h3{
  369. color: RoyalBlue;
  370. font-size: 14px;
  371. font-weight: normal;
  372. margin: 0 0 0.2em 0;
  373. }
  374. table .no {
  375. color: #FFFFFF;
  376. font-size: 14px;
  377. background: RoyalBlue;
  378. }
  379. table .desc {
  380. text-align: left;
  381. }
  382. table .unit {
  383. background: #DDDDDD;
  384. }
  385. table .qty {
  386. }
  387. table .total {
  388. background: RoyalBlue;
  389. color: #FFFFFF;
  390. }
  391. table td.unit,
  392. table td.qty,
  393. table td.total {
  394. font-size: 14px;
  395. }
  396. table tbody tr:last-child td {
  397. border: none;
  398. }
  399. table tfoot td {
  400. padding: 10px 20px;
  401. background: #FFFFFF;
  402. border-bottom: none;
  403. font-size: 16px;
  404. white-space: nowrap;
  405. border-top: 1px solid #AAAAAA;
  406. }
  407. table tfoot tr:first-child td {
  408. border-top: none;
  409. }
  410. table tfoot tr:last-child td {
  411. color: RoyalBlue;
  412. font-size: 16px;
  413. border-top: 1px solid RoyalBlue;
  414. }
  415. table tfoot tr td:first-child {
  416. border: none;
  417. }
  418. # thanks{
  419. font-size: 16px;
  420. margin-bottom: 50px;
  421. }
  422. # notes{
  423. padding-left: 6px;
  424. border-left: 6px solid RoyalBlue;
  425. }
  426. # notes .note {
  427. font-size: 16px;
  428. }
  429. footer {
  430. color: #777777;
  431. width: 100%;
  432. height: 30px;
  433. position: absolute;
  434. bottom: 0;
  435. border-top: 1px solid #AAAAAA;
  436. padding: 8px 0;
  437. text-align: center;
  438. }
  439. </style>
  440. "
  441.  
  442. Const c_tableTpl = "
  443. <!DOCTYPE html>
  444. <html lang='en'>
  445. <head><meta charset='utf-8'>{12}</head>
  446. <body>
  447. <header class='clearfix'>
  448. <div id = 'company'>
  449. <h2 class='name'>{1}</h2>
  450. <div>{2}</div>
  451. <div>{3}</div>
  452. <div><a href = '{4}'> {4}</a></div>
  453. </div>
  454. </header>
  455. <main>
  456. <div id='details' class='clearfix'>
  457. <div id='client'>
  458. <div class='to'>INVOICE TO:</div>
  459. <h2 class='name'>{5}</h2>
  460. <div class='address'>{6}</div>
  461. </div>
  462. <div id='invoice'>
  463. <h1>INVOICE</h1>
  464. <div class='date'>Date of Invoice: {7}</div>
  465. <div class='date'>Due Date: {8}</div>
  466. </div>
  467. </div>
  468. <table border='0' cellspacing='0' cellpadding='0'>
  469. <thead>
  470. <tr>
  471. <th class='no'>#</th>
  472. <th class='desc'>DESCRIPTION</th>
  473. <th class='unit'>UNIT PRICE</th>
  474. <th class='qty'>QUANTITY</th>
  475. <th class='total'>TOTAL</th>
  476. </tr>
  477. </thead>
  478. <tbody>
  479. {0}
  480. </tbody>
  481. <tfoot>
  482. <tr>
  483. <td colspan='2'></td>
  484. <td colspan='2'>SUBTOTAL</td>
  485. <td>{9}</td>
  486. </tr>
  487. <tr>
  488. <td colspan='2'></td>
  489. <td colspan='2'>TAX 25%</td>
  490. <td>{10}</td>
  491. </tr>
  492. <tr>
  493. <td colspan='2'></td>
  494. <td colspan='2'>GRAND TOTAL</td>
  495. <td>{11}</td>
  496. </tr>
  497. </tfoot>
  498. </table>
  499. <div id='thanks'>Thank you!</div>
  500. <div id='notes'>
  501. <div>NOTES:</div>
  502. <div class='note'></div>
  503. </div>
  504. </main>
  505. </body>
  506. </html>
  507. "
  508. Const c_dataRowFmt = "
  509. <tr>
  510. <td class='no'>{0}</td>
  511. <td class='desc'><h3>{1}</h3></td>
  512. <td class='unit'>{2}</td>
  513. <td class='qty'>{3}</td>
  514. <td class='total'>{4}</td>
  515. </tr>
  516. "
  517. End Class
  518.