The goal of this tutorial is to send an order confirmation
by email directly from SAP transaction VA03. A pushbutton
calls a .NET method that retrieves the order data via BAPI and
sends it as a formatted HTML email.
We first add a pushbutton to the SAP screen:
GuiXT
pushbutton (3,22) "@J8\QSend order as e-mail@Send as E-Mail " _
process="send_email_order_va03.txt"
In the process script send_email_order_va03.txt we first
set a variable for the CC address, then call the .NET method
asynchronously via callvbasync. The current order
number is passed as a GuiXT field value:
GuiXT
// CC copy of the email to
Set V[ccRef] "your@email.com"
// Send order summary by email
callvbasync tutorials.utilities.send_order_html "&F[Order]" _
"your@email.com" "Synactive Employee" "ccRef" _
"Order &F[Order]"
For GuiXT to call .NET methods, the guinet.dll must be
included as a project reference in the class library:
The script fetchData is called by the .NET method via
g.Process(). It retrieves the order header data,
line items and address information via BAPI
BAPI_SALESORDER_GETDETAILBOS. The three data areas
are joined into a single return string using the delimiter
<|>:
GuiXT (fetchData)
PARAMETER vbeln
// Order header
CreateStructure V[orderheader] _
sold_to doc_date purch_no net_val_hd currency purch_date
// Items
CreateTable V[orderitems] _
itm_number material short_text req_qty sales_unit subtot_pp1
// Addresses
CreateStructure V[orderaddress] name street country postl_code city
CreateTable V[orderaddresses] include=V[orderaddress]
Call "BAPI_SALESORDER_GETDETAILBOS" _
export.SALESDOCUMENT="vbeln" _
import.ORDERHEADER="orderheader" _
import.ORDERADDRESS="orderaddresses" _
import.ORDERITEMS="orderitems"
return "&V[orderheader]<|>&V[orderitems]<|>&V[orderaddresses]"
The Email class contains the SMTP configuration
and the send call. send_order creates a
MailMessage with an optional CC recipient, enables
SSL over TLS 1.2/1.3 and sends the email via an
SmtpClient:
Imports System.Globalization
Imports System.Net
Imports System.Net.Mail
Imports System.Text.Json.Nodes
Imports guinet
Namespace Emails
Public Class Email
Private ReadOnly smtpHost As String = "your.server.de"
Private ReadOnly smtpPort As Integer = 587
Private ReadOnly smtpUser As String = "your@email.com"
Private ReadOnly smtpPass As String = "password"
Public Sub send_order(
toEmail As String,
toName As String,
ccRef As String,
subject As String,
html As String)
Dim mail As New MailMessage()
mail.From = New MailAddress(smtpUser, "Shop")
mail.To.Add(New MailAddress(toEmail, toName))
If Not String.IsNullOrEmpty(ccRef) Then
mail.CC.Add(ccRef)
End If
mail.Subject = subject
mail.Body = html
mail.IsBodyHtml = True
Dim smtp As New SmtpClient(smtpHost, smtpPort)
smtp.Credentials = New NetworkCredential(
smtpUser,
smtpPass
)
smtp.EnableSsl = True
System.Net.ServicePointManager.SecurityProtocol =
SecurityProtocolType.Tls12 Or
SecurityProtocolType.Tls13
smtp.Send(mail)
End Sub
End Class
' ...
using guinet;
using System;
using System.Globalization;
using System.Net;
using System.Net.Mail;
using System.Text.Json.Nodes;
namespace Emails
{
public class Email
{
private readonly string smtpHost = "your.server.de";
private readonly int smtpPort = 587;
private readonly string smtpUser = "your@email.com";
private readonly string smtpPass = "password";
public void send_order(
string toEmail,
string toName,
string ccRef,
string subject,
string html)
{
MailMessage mail = new MailMessage();
mail.From = new MailAddress(smtpUser, "Shop");
mail.To.Add(new MailAddress(toEmail, toName));
if (!string.IsNullOrEmpty(ccRef))
mail.CC.Add(ccRef);
mail.Subject = subject;
mail.Body = html;
mail.IsBodyHtml = true;
SmtpClient smtp = new SmtpClient(smtpHost, smtpPort);
smtp.Credentials = new NetworkCredential(
smtpUser,
smtpPass
);
smtp.EnableSsl = true;
System.Net.ServicePointManager.SecurityProtocol =
SecurityProtocolType.Tls12 |
SecurityProtocolType.Tls13;
smtp.Send(mail);
}
}
// ...
The SMTPUtils class handles the entire HTML email
assembly. The helper method format normalizes SAP
decimal numbers: SAP returns amounts with a comma as the decimal
separator (e.g. 1234,56), while Single.Parse
with CultureInfo.InvariantCulture expects a dot. The
comma is therefore replaced, the value parsed, and returned with
two fixed decimal places.
send_order_html is the entry point called by
callvbasync. Via
g.Process("fetchData", orderno), GuiXT executes the
fetchData script in the context of the current SAP session
and returns the result string directly. The delimiter
<|> between the three JSON blocks was chosen
because it cannot appear in any SAP field value. After splitting,
header data, line items and address are parsed as separate
JsonNode objects and the order summary is written
to the HTML body:
' ...
Public Class SMTPUtils
' Normalizes SAP comma decimals to "0.00" format
Public Function format(input As String) As String
input = input.Replace(",", ".")
Dim val As Single = Single.Parse(
input, CultureInfo.InvariantCulture)
Return val.ToString(
"0.00", CultureInfo.InvariantCulture)
End Function
Public Function send_order_html(
orderno As String,
toEmail As String,
toName As String,
ccRef As String,
subject As String) As String
Dim g As New guixt()
' Build HTML document head
Dim HTML As String =
"<!doctype html><html><head>" &
"<meta charset='utf-8'></meta>"
Try
HTML &= table_add_css() & "</head>"
HTML &= "<body><hr>" &
"Order summary for order " &
orderno & "<br><hr><br><br>"
' g.Process executes the fetchData GuiXT script
Dim data As String = g.Process("fetchData", orderno)
' <|> is used as separator — never appears in data
Dim data_arr As String() = data.Split(
New String() {"<|>"},
StringSplitOptions.None
)
' Parse three JSON blocks: header, items, address
Dim oHeader As JsonNode = JsonNode.Parse(data_arr(0))
Dim oItems As JsonArray =
JsonNode.Parse(data_arr(1)).AsArray()
Dim oAddr As JsonNode =
JsonNode.Parse(data_arr(2))?(0)
' Build address string from first address entry
Dim address As String =
$"{oAddr("name")}/{oAddr("street")}/" &
$"{oAddr("country")}-" &
$"{oAddr("postl_code")} {oAddr("city")}"
' Write order summary lines to HTML body
HTML &= "Net value: " &
format(oHeader("net_val_hd").ToString()) & " "
HTML &= oHeader("currency").ToString() & "<br>"
HTML &= "Sold-to: " &
address & "<br>"
HTML &= "Ship-to: " &
oHeader("sold_to").ToString() & "<br>"
HTML &= "PO number: " &
oHeader("purch_no").ToString() & "<br>"
HTML &= "PO date: " &
oHeader("purch_date").ToString() & "<br>"
HTML &= "<br>Line items:<br><br>"
' ...
// ...
public class SMTPUtils
{
// Normalizes SAP comma decimals to "0.00" format
public string format(string input)
{
input = input.Replace(",", ".");
float val = float.Parse(
input, CultureInfo.InvariantCulture);
return val.ToString(
"0.00", CultureInfo.InvariantCulture);
}
public string send_order_html(
string orderno,
string toEmail,
string toName,
string ccRef,
string subject)
{
guixt g = new guixt();
// Build HTML document head
string HTML =
"<!doctype html><html><head>" +
"<meta charset='utf-8'></meta>";
try
{
HTML += table_add_css() + "</head>";
HTML += "<body><hr>" +
"Order summary for order " +
orderno + "<br><hr><br><br>";
// g.Process executes the fetchData GuiXT script
string data = g.Process("fetchData", orderno);
// <|> is used as separator — never appears in data
string[] data_arr = data.Split(
new[] { "<|>" },
StringSplitOptions.None
);
// Parse three JSON blocks: header, items, address
JsonNode oHeader = JsonNode.Parse(data_arr[0]);
JsonArray oItems =
JsonNode.Parse(data_arr[1]).AsArray();
JsonNode oAddr =
JsonNode.Parse(data_arr[2])?[0];
// Build address string from first address entry
string address =
$"{oAddr["name"]}/{oAddr["street"]}/" +
$"{oAddr["country"]}-" +
$"{oAddr["postl_code"]} {oAddr["city"]}";
// Write order summary lines to HTML body
HTML += "Net value: " +
format(oHeader["net_val_hd"].ToString()) + " ";
HTML += oHeader["currency"] + "<br>";
HTML += "Sold-to: " +
address + "<br>";
HTML += "Ship-to: " +
oHeader["sold_to"] + "<br>";
HTML += "PO number: " +
oHeader["purch_no"] + "<br>";
HTML += "PO date: " +
oHeader["purch_date"] + "<br>";
HTML += "<br>Line items:<br><br>";
// ...
The items table is built entirely as an HTML string. SAP returns
position numbers with leading zeros (e.g. 000010).
The value is therefore converted to an integer via
Integer.Parse and then back to a string — this
reliably removes the leading zeros without string manipulation.
The subtotal from BAPI field subtot_pp1 is normalized
via format and written to the last column together
with the currency from the header data:
' ...
' Build items table
HTML &= "<table cellspacing='0'><thead><tr>"
HTML &= "<th>Pos</th>"
HTML &= "<th>Mat.No.</th>"
HTML &= "<th>Qty</th>"
HTML &= "<th>Unit</th>"
HTML &= "<th>Description</th>"
HTML &= "<th>Subtotal</th>"
HTML &= "</tr></thead><tbody>"
For Each item As JsonObject In oItems
' Integer.Parse removes SAP leading zeros e.g. "000010" -> "10"
Dim itmNum = item("itm_number").ToString()
Dim pos = Integer.Parse(itmNum).ToString()
HTML &= "<tr>"
HTML &= "<td>" & pos & "</td>"
HTML &= "<td>" &
item("material").ToString() & "</td>"
HTML &= "<td>" &
item("req_qty").ToString() & "</td>"
HTML &= "<td>" &
item("sales_unit").ToString() & "</td>"
HTML &= "<td>" &
item("short_text").ToString() & "</td>"
HTML &= "<td>" &
format(item("subtot_pp1").ToString()) &
" " & oHeader("currency").ToString() &
"</td>"
HTML &= "</tr>"
Next
HTML &= "</tbody></table>"
' ...
// ...
// Build items table
HTML += "<table cellspacing='0'><thead><tr>";
HTML += "<th>Pos</th>";
HTML += "<th>Mat.No.</th>";
HTML += "<th>Qty</th>";
HTML += "<th>Unit</th>";
HTML += "<th>Description</th>";
HTML += "<th>Subtotal</th>";
HTML += "</tr></thead><tbody>";
foreach (JsonObject item in oItems)
{
// Integer.Parse removes SAP leading zeros e.g. "000010" -> "10"
string itmNum = item["itm_number"].ToString();
int pos = int.Parse(itmNum);
HTML += "<tr>";
HTML += "<td>" + pos + "</td>";
HTML += "<td>" +
item["material"] + "</td>";
HTML += "<td>" +
item["req_qty"] + "</td>";
HTML += "<td>" +
item["sales_unit"] + "</td>";
HTML += "<td>" +
item["short_text"] + "</td>";
HTML += "<td>" +
format(item["subtot_pp1"].ToString()) +
" " + oHeader["currency"] +
"</td>";
HTML += "</tr>";
}
HTML += "</tbody></table>";
// ...
If an error occurs, send_order_html returns the
exception message as a return value — GuiXT then displays this
string as the result of callvbasync, making
debugging easier.
table_add_css returns a <style>
block embedded directly in the HTML body. This is necessary
because most email clients — including Outlook and Gmail —
ignore or strip CSS in the <head>. CSS in
the body ensures the table is rendered correctly in all clients:
' ...
Catch ex As Exception
Return ex.Message
End Try
Dim e As New Email()
e.send_order(toEmail, toName, ccRef, subject, HTML)
Return HTML
End Function
Private Function table_add_css() As String
Return "
<style>
table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
th {
background-color: #f4f4f4;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #fafafa;
}
tr:hover {
background-color: #f1f1f1;
}
</style>"
End Function
End Class
End Namespace