ZugferdInvoice.cs
//
// This code is part of Document Solutions for PDF demos.
// Copyright (c) MESCIUS inc. All rights reserved.
//
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
{
// 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 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");
// Collect order data:
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;
// Build HTML to be converted to PDF:
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);
// Build ZUGFeRD compliant XML to attach to the PDF:
var 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(it =>
{
zugferdDesc.AddTradeLineItem(
name: it.ItemDescription,
billedQuantity: it.Quantity,
netUnitPrice: it.Rate,
unitCode: QuantityCodes.C62);
});
// Save the invoice info XML:
using var ms = new MemoryStream();
zugferdDesc.Save(ms, ZUGFeRDVersion.Version1, Profile.Basic);
ms.Seek(0, SeekOrigin.Begin);
var tmpPdf = Path.GetTempFileName();
// Create an instance of GcHtmlBrowser that is used to render HTML:
using var browser = Common.Util.NewHtmlBrowser();
using var htmlPage = browser.NewPage(html);
// Set up HTML headers, margins etc (see 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>"
};
// Render the source Web page to the temporary file:
htmlPage.SaveAsPdf(tmpPdf, pdfOptions);
// Create the result PDF as a PDF/A-3 compliant document with the ZUGFeRD XML attachment:
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";
// According to the ZUGFeRD 1.x standard naming, the filename should be 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);
}
// Copy the created PDF from the temp file to target stream:
using (var ts = File.OpenRead(tmpZugferd))
ts.CopyTo(stream);
// Clean up:
File.Delete(tmpZugferd);
File.Delete(tmpPdf);
// Done.
}
// Some records in our sample database lack some dates:
private static DateTime ConvertToDateTime(object value)
{
if (Convert.IsDBNull(value))
return DateTime.MinValue;
else
return Convert.ToDateTime(value);
}
// Provide ZUGFeRD country codes:
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' is not present in s_regions but is used by our sample database:
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 styles and templates used to render the invoice:
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>
";
}
}