ZugferdInvoice.cs
// 完毕:
using System;
using System.IO;
using System.Drawing;
using System.Text;
using System.Data;
using System.Linq;
using System.Collections.Generic;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Html;
using System.Globalization;
using s2industries.ZUGFeRD;
namespace DsPdfWeb.Demos
{
// 此示例使用 DsNWind 示例数据库创建 PDF 发票,
// 并将根据 ZUGFeRD 1.x 标准规则创建的 XML 文件附加到其中。
//
// ZUGFeRD 是基于 PDF 和 XML 文件格式的德国电子发票标准。
// 它准备改变发票的处理方式,并可供任何类型的企业使用。
// 它将使发件人和客户的发票处理更加高效。
// 详细信息请参见什么是ZUGFeRD? 。
//
// 此示例使用 ZUGFeRD-csharp 包
// 创建附加到发票的 ZUGFeRD 兼容 XML。
//
// 有关使用 GcDocs.Html 将 HTML 渲染为 PDF 的详细信息,请参阅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");
// 收集订单数据:
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;
// 构建要转换为 PDF 的 HTML:
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);
// 构建 ZUGFeRD 兼容的 XML 以附加到 PDF:
var 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(it =>
{
zugferdDesc.AddTradeLineItem(
name: it.ItemDescription,
billedQuantity: it.Quantity,
netUnitPrice: it.Rate,
unitCode: QuantityCodes.C62);
});
// 保存发票信息 XML:
using var ms = new MemoryStream();
zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic);
ms.Seek(0, SeekOrigin.Begin);
var tmpPdf = Path.GetTempFileName();
// 创建一个用于呈现 HTML 的 GcHtmlBrowser 实例:
using var browser = Common.Util.NewHtmlBrowser();
using var htmlPage = browser.NewPage(html);
// 设置 HTML 标题、边距等(参见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>"
};
// 将源网页渲染到临时文件:
htmlPage.SaveAsPdf(tmpPdf, pdfOptions);
// 使用 ZUGFeRD XML 附件将结果 PDF 创建为 PDF/A-3 兼容文档:
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";
// 根据 ZUGFeRD 1.x 标准命名,文件名应为 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);
}
// 将创建的 PDF 从临时文件复制到目标流:
using (var ts = File.OpenRead(tmpZugferd))
ts.CopyTo(stream);
// 清理:
File.Delete(tmpZugferd);
File.Delete(tmpPdf);
// 完毕:
}
// 我们的示例数据库中的一些记录缺少一些日期:
private static DateTime ConvertToDateTime(object value)
{
if (Convert.IsDBNull(value))
return DateTime.MinValue;
else
return Convert.ToDateTime(value);
}
// 提供 ZUGFeRD 国家代码:
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”不存在于 s_regions 中,但被我们的示例数据库使用:
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 样式和模板:
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>
";
}
}