ZugferdInvoice.vb
'' 完毕:
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
'' 此示例使用 DsNWind 示例数据库创建 PDF 发票,
'' 并将根据 ZUGFeRD 1.x 标准规则创建的 XML 文件附加到其中。
''
'' ZUGFeRD 是基于 PDF 和 XML 文件格式的德国电子发票标准。
'' 它准备改变发票的处理方式,并可供任何类型的企业使用。
'' 它将使发件人和客户的发票处理更加高效。
'' 详细信息请参见什么是ZUGFeRD? 。
''
'' 此示例使用 ZUGFeRD-csharp 包
'' 创建附加到发票的 ZUGFeRD 兼容 XML。
''
'' 有关使用 GcDocs.Html 将 HTML 渲染为 PDF 的详细信息,请参阅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")
'' 收集订单数据:
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
'' 构建要转换为 PDF 的 HTML:
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)
'' 构建 ZUGFeRD 兼容的 XML 以附加到 PDF:
Dim zugferdDesc = InvoiceDescriptor.CreateInvoice(
order.OrderID.ToString(),
order.OrderDate,
CurrencyCodes.USD)
'' 填写发票买家信息:
zugferdDesc.SetBuyer(
order.ShipName,
order.ShipPostalCode,
order.ShipCity,
order.ShipAddress,
GetCountryCode(order.ShipCountry),
order.CompanyName)
zugferdDesc.SetBuyerContact($"{order.FirstName} {order.LastName}")
'' 填写发票卖家信息:
zugferdDesc.SetSeller(
supplier.CompanyName,
supplier.PostalCode,
supplier.City,
supplier.Address,
GetCountryCode(supplier.Country),
supplier.CompanyName)
'' 交货日期和总计:
zugferdDesc.ActualDeliveryDate = order.RequiredDate
zugferdDesc.SetTotals(orderSubTotal, orderTax, allowanceTotalAmount, taxBasisTotalAmount, taxTotalAmount, grandTotalAmount)
'' 付款仓位部分:
orderDetails.ForEach(
Sub(it)
zugferdDesc.AddTradeLineItem(name:=it.ItemDescription,
billedQuantity:=it.Quantity,
netUnitPrice:=it.Rate,
unitCode:=QuantityCodes.C62)
End Sub)
'' 保存发票信息 XML:
Using ms = New MemoryStream()
zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic)
ms.Seek(0, SeekOrigin.Begin)
Dim tmpPdf = Path.GetTempFileName()
'' 创建一个用于呈现 HTML 的 GcHtmlBrowser 实例:
Using browser = Util.NewHtmlBrowser(), htmlPage = browser.NewPage(html)
'' 设置 HTML 标题、边距等(参见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>"
}
'' 将源网页渲染到临时文件:
htmlPage.SaveAsPdf(tmpPdf, pdfOptions)
End Using
'' 使用 ZUGFeRD XML 附件将结果 PDF 创建为 PDF/A-3 兼容文档:
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"
'' 根据 ZUGFeRD 1.x 标准命名,文件名应为 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)
'' 附件字典键可以是任何内容:
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
'' 将创建的 PDF 从临时文件复制到目标流:
Using ts = File.OpenRead(tmpZugferd)
ts.CopyTo(stream)
End Using
'' 清理:
File.Delete(tmpZugferd)
File.Delete(tmpPdf)
End Using
End Using
'' 完毕:
End Sub
'' 我们的示例数据库中的一些记录缺少一些日期:
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
'' 提供 ZUGFeRD 国家代码:
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”不存在于 s_regions 中,但被我们的示例数据库使用:
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 样式和模板:
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