In D365FO you can set up Email templates for sending email notifications for Retail orders, workflows, alerts, etc.
With Docentric AX Free Edition you can:
- use an advanced HTML editor to format email bodies within D365FO,
- pick a placeholder from a list of available placeholders for the current email, if you need to create a dynamic email body.
Learn the basic stuff about Email Templates improved with Docentric >>
In this article we will demonstrate how to create and send a dynamic email body with a dynamic table, e.g. a Sales Order Confirmation email, which will contain some header/footer dynamic information such as Customer name and Sales ID but also a list of all ordered items and their prices.
First, we need to create a custom Docentric email handler class, which will provide the list of both header and line placeholders. This class can also contain logic for suppling the values for these placeholders. Then we need to set up an email template using Docentric and the created custom email handler class. In the end we will discuss options on how to send an email based on the created email template.
Create custom email handler class
Create a new X++ class that extends the DocEmailTemplateHandlerBase class and implement at least two methods: description() and defineCustomPlaceholders():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
class DocOrderConfirmWithLinesEmailHandler extends DocEmailTemplateHandlerBase { // Placeholders for sales order summary (i.e. header/footer) public const str PlaceholderCustomerName = 'customername'; public const str PlaceholderSalesId = 'salesid'; public const str PlaceholderDeliveryAddress = 'deliveryaddress'; public const str PlaceholderCustomerAddress = 'customeraddress'; public const str PlaceholderDeliveryDate = 'deliverydate'; public const str PlaceholderShipDate = 'shipdate'; public const str PlaceholderModeOfDelivery = 'modeofdelivery'; public const str PlaceholderCharges = 'charges'; public const str PlaceholderTax = 'tax'; public const str PlaceholderTotal = 'total'; public const str PlaceholderDiscount = 'discount'; // Placeholders for sales order detail (i.e. lines) public const str PlaceholderProductName = 'lineproductname'; public const str PlaceholderProductDescription = 'lineproductdescription'; public const str PlaceholderQuantity = 'linequantity'; public const str PlaceholderLineUnit = 'lineunit'; public const str PlaceholderPrice = 'lineprice'; public const str PlaceholderNetAmount = 'linenetamount'; public const str PlaceholderLineDiscount = 'linediscount'; public const str PlaceholderLineShipDate = 'lineshipdate'; public const str PlaceholderLineDeliveryDate = 'linedeliverydate'; public const str PlaceholderLineDeliveryMode = 'linedeliverymode'; public const str PlaceholderLineDeliveryAddress = 'linedeliveryaddress'; public ClassDescription description() { return 'Order Confirmation With Lines email handler class'; } /**********************************************************************************/ /* Defining Custom Placeholders */ /**********************************************************************************/ protected void defineCustomPlaceholders(Map _customPlaceholderDefinitionMap) { // Placeholders for sales order summary (i.e. header/footer) _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCustomerName, 'CO - Customer name'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderSalesId, 'CO - Sales ID'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDeliveryAddress, 'CO - Delivery address'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCustomerAddress, 'CO - Customer address'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDeliveryDate, 'CO - Delivery date'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderShipDate, 'CO - Shipping date'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderModeOfDelivery, 'CO - Mode of delivery'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCharges, 'CO - Charges'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderTax, 'CO - Tax'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderTotal, 'CO - Total'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDiscount, 'CO - Discount'); // Placeholders for sales order detail (i.e. lines) _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderProductName, 'COL - Product name'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderProductDescription, 'COL - Product description'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderQuantity, 'COL - Qty'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderPrice, 'COL - Product price'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderNetAmount, 'COL - Line amount'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDiscount, 'COL - Line discount'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineShipDate, 'COL - Line shipping date'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryDate, 'COL - Line delivery date'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryMode, 'COL - Line delivery mode'); _customPlaceholderDefinitionMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryAddress, 'COL - Line delivery address'); } } |
Set up email template with custom email handler class
Navigate to Organization administration -> Setup -> Email templates. Click the Docentric settings tab page and turn on the Use Docentric email editor checkbox. Then select previously created DocOrderConfirmWithLinesEmailHandler class (you have to build the belonging model first) as Class for placeholders.
After you click the Edit button located above the Email message content grid, the form with Docentric email body editor is open. You can see that the Field dropdown list contains placeholders defined by your DocOrderConfirmWithLinesEmailHandler class.
You will design the email template more easily if you switch to Full Screen.
You can finish designing your template using the email editor's formatting features and both header and line placeholders regularly. Notice that in the custom email handler class we created placeholders for header with friendly names starting with CO (Confirmation Order) while placeholders for lines have friendly names starting with COL (Confirmation Order Line).
After we finish the whole template, we need to take some additional steps to enable dynamic lines, i.e. dynamic table.
Inserting hidden placeholders for the repeating table row
Scroll through the placeholder's dropdown list and find two special placeholders:
- Hidden table begin tag
- Hidden table end tag
Insert them temporarily in the email body and follow the instructions on the screenshots below.
Sending emails with a dynamic table in the body
In D365 only Retail orders support such a scenario with a dynamic table in the body, and if you are using this built-in logic for sending email notifications for retail orders you will need to add two more placeholders to enable tagging of the repeating table row, in addition to all other custom placeholders in your email handler class:
- tablebegin.salesline
- tableend.salesline
You need to use these two placeholder instead of <!‐‐%HIDDEN_TABLEBEGIN%‐‐> and <!‐‐%HIDDEN_TABLEEND%‐‐> to wrap the repeating table row in an email body with a dynamic table, following the instructions from the previous section.
Check the RetailOENInfo built-in class – you will find there actually the whole list of all header and line placeholders for Retail orders.
However, if you want to send emails with a dynamic table in the body in some other scenarios, you can use Docentric APIs for sending emails, or write your own code.
Sending emails using Docentric APIs
You can use the DocEmailTemplateManager::sendMail() method to send emails with dynamic table in the body:
1 2 3 4 5 6 7 8 9 10 11 |
public static void testSendEmail() { str recipient = 'sara@gmail.com'; str salesId = '000725'; str languageId = 'en-us'; str emailId = 'CnfmOrdExt'; // Send an email based on a template with the given emailId. DocEmailTemplateManager::sendMail(emailId, languageId, recipient, null, null, [salesId]); info(strFmt('Email based on template %1 successfully sent to %2!', emailId, recipient)); } |
In order for this to work your custom email handler class should contain logic for supplying values for both header and line placeholders, i.e. it should implement the fillMappingsWithCustomPlaceholderValues() and fillLineMappingsWithCustomPlaceholderValues() methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
class DocOrderConfirmWithLinesEmailHandler extends DocEmailTemplateHandlerBase { ... /**********************************************************************************/ /* Supplying values for Custom Placeholders */ /**********************************************************************************/ public void fillMappingsWithCustomPlaceholderValues(Map _mappings /* Map(PlaceholderName (str) -> PlaceholderValue (str)) */, container _contextInfo = conNull()) { str salesId = conPeek(_contextInfo, 1); SalesTable salesOrder = SalesTable::find(salesId); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCustomerName, salesOrder.SalesName); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderSalesId, salesOrder.SalesId); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDeliveryAddress, salesOrder.deliveryAddressing()); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCustomerAddress, CustTable::find(salesOrder.CustAccount).address()); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDeliveryDate, DocOrderConfirmWithLinesEmailHandler::formatDatetimeData(salesOrder.deliveryDateDisplay(), languageId)); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderShipDate, DocOrderConfirmWithLinesEmailHandler::formatDatetimeData(salesOrder.ShippingDateRequested, languageId)); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderModeOfDelivery, dlvMode::find(salesOrder.DlvMode).Txt); AmountCur totalAmount, totalTaxAmount, totalDiscount, totalCharge; try { SalesTotals salesTotals = salesTotals::construct(salesOrder, SalesUpdate::All); salesTotals.calc(); totalCharge = salesTotals.totalMarkup(); totalAmount = salesTotals.totalAmount() + salesOrder.amountInvoiced(); totalTaxAmount = salesTotals.totalTaxAmount(); totalDiscount = salesTotals.totalLineDisc(); } catch { totalCharge = 0; totalAmount = 0; totalTaxAmount = 0; totalDiscount = 0; } _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCharges, DocOrderConfirmWithLinesEmailHandler::formatNumericData(totalCharge, languageId)); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderTax, DocOrderConfirmWithLinesEmailHandler::formatNumericData(totalTaxAmount, languageId)); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderTotal, DocOrderConfirmWithLinesEmailHandler::formatNumericData(totalAmount, languageId)); _mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderDiscount, DocOrderConfirmWithLinesEmailHandler::formatNumericData(totalDiscount, languageId)); } public void fillLineMappingsWithCustomPlaceholderValues(List _lineMappings /* List of Map(PlaceholderName -> PlaceholderValue) */, Map _mappings /* Map(PlaceholderName (str) -> PlaceholderValue (str)) */, container _contextInfo = conNull()) { str salesId = conPeek(_contextInfo, 1); SalesLine salesLine; while select salesLine where salesLine.SalesId == salesId { Map salesLineMap = new Map(Types::String, Types::String); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderProductName, salesLine.itemName()); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderProductDescription, EcoResProductTranslation::getDescriptionOrDefaultDescription(InventTable::find(salesLine.ItemId).Product, CompanyInfo::languageId())); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderQuantity, DocOrderConfirmWithLinesEmailHandler::formatNumericData(salesLine.SalesQty, languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderPrice, DocOrderConfirmWithLinesEmailHandler::formatNumericData(salesLine.SalesPrice, languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDiscount, DocOrderConfirmWithLinesEmailHandler::formatNumericData(salesLine.LineDisc, languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderNetAmount, DocOrderConfirmWithLinesEmailHandler::formatNumericData(salesLine.LineAmount, languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryDate, DocOrderConfirmWithLinesEmailHandler::formatDatetimeData(salesLine.deliveryDate(), languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineShipDate, DocOrderConfirmWithLinesEmailHandler::formatDatetimeData(salesLine.ShippingDateRequested, languageId)); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryMode, DlvMode::find(salesLine.DlvMode).Txt); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineDeliveryAddress, salesLine.deliveryAddress().Address); salesLineMap.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderLineUnit, salesLine.SalesUnit); _lineMappings.addEnd(salesLineMap); } } } |
If your custom email handler class doesn't provide values for header and line placeholders, i.e. it doesn't implement the fillMappingsWithCustomPlaceholderValues() and fillLineMappingsWithCustomPlaceholderValues() methods, then you first have to fill the mappings Placeholder name -> Placeholder value for both header and line placeholders, and afterwards to use the DocEmailTemplateManager::sendMail() method in order to send an email with a dynamic table in the body, as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static void testSendEmail() { str recipient = 'sara@gmail.com'; str salesId = '000725'; str languageId = 'en-us'; str emailId = 'CnfmOrdExt'; // Create and fill mappings with placeholder values outside the corresponding // custom email handler class. Map mappings = DocOrderConfirmWithLinesEmailHandler::createAndFillMappingsFromOutside(salesId, languageId); List lineMappings = DocOrderConfirmWithLinesEmailHandler::createAndFillLineMappingsFromOutside(salesId, languageId); // Send an email based on a template with the given emailId. DocEmailTemplateManager::sendMail(emailId, languageId, recipient, mappings, lineMappings); info(strFmt('Email based on template %1 successfully sent to %2!', emailId, recipient)); } |
Sending emails using your own X++ method
You can also use your own logic for sending emails with a dynamic table in the body but you need first to fill mappings for header and line placeholders using the DocEmailTemplateHandlerBase::fillMappings() method, and afterwards to replace all placeholders in the email body and subject using the DocEmailTemplateManager::generateEmailBodyAndSubjectWithDynamicLines() method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public static void testSendEmail() { str recipient = 'sara@gmail.com'; str salesId = '000725'; str languageId = 'en-us'; str emailId = 'CnfmOrdExt'; // Step #1: Get the email table and message for the given emailId and languageId. SysEmailMessageTable sysEmailMessageTable = SysEmailMessageTable::find(emailId, languageId); // Step #2: Create and fill a mapping between placeholders and their values. Map mappings = new Map(Types::String, Types::String); // Map(PlaceholderName (str) -> PlaceholderValue (str)) List lineMappings = new List(Types::Class); // List of Map(PlaceholderName (str) -> PlaceholderValue (str)) DocEmailTemplateHandlerBase::fillMappings(emailId, languageId, mappings, false, [salesId], lineMappings); // Step #2b: (OPTIONAL) Additionally, fill mappings and lineMappings with values of your custom // placeholders but only in case that your email handler class doesn't already contain logic for // providing these values, i.e. you didn't implement the fillMappingsWithCustomPlaceholderValues() // and fillLineMappingsWithCustomPlaceholderValues() methods but you prefer to fill mappings // and lineMappings outside your email handler class. SalesTable salesTable = SalesTable::find(salesId); mappings.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderCustomerName, salesTable.SalesName); ... SalesLine salesLine; while select salesLine where salesLine.SalesId == salesId { Map mappingsForCurrentLine = new Map(Types::String, Types::String); // Map(PlaceholderName (str) -> PlaceholderValue (str)) lineMappings.addEnd(mappingsForCurrentLine); mappingsForCurrentLine.insert(DocOrderConfirmWithLinesEmailHandler::PlaceholderProductName, salesLine.itemName()); ... } // Step #3: Generate the email body and subject by replacing all placeholders whose values are // provided in mappings and lineMappings. It will automatically multiply all dynamic lines in // the email body, i.e. a html part (e.g. table row) wrapped in the %HIDDEN_TABLEBEGIN% // and %HIDDEN_TABLEEND% placeholders. str emailBody, emailSubject; [emailBody, emailSubject] = DocEmailTemplateManager::generateEmailBodyAndSubjectWithDynamicLines( sysEmailMessageTable.Mail, sysEmailMessageTable.Subject, mappings, lineMappings); // Step #4: Send the email. YourClass::sendMail(recipient, emailBody, emailSubject, ...); } |
Resources
Download custom email handler class >>
Learn about sending emails using Docentric APIs >>
Read how-to manual on improved Email templates >>
Hi Team,
I would like to know on how feasible it works for generating data from join tables and display method or calculation values.
Regards,
Ravisankar
Hi Ravisankar,
You can achieve quite complex scenarios using Email templates but in order to help you, please describe your concrete scenario. You can also contact us at support@docentric.com. We are always eager to hear about different use cases!
Kind regards,
Ana
Hi Team,
How can I hide the dynamic table when there is no data in it?
Example:
The dynamic table has 0 rows, but it still shows the header and values as zero. I need to hide the table when there is no data.
Hi,
Before creating and sending an email message, you can check if there is no data (lines) used for populating the dynamic table in the email template, so in that case you can use a different email template without the dynamic table.
Alternatively, you can create a pre-event handler for the DocEmailTemplateManager::generateEmailBodyAndSubjectWithDynamicLines() method and pre-process the email body with placeholders as a string containing HTML to remove the empty HTML table. This code could look like this:
const str repeatingBlockMarkEnd = '<!‐‐%HIDDEN_TABLEEND%‐‐>';
int posEnd = strScan(_emailBodyWithPlaceholders, repeatingBlockMarkEnd, 1,
{
emailBody_FirstPart = subStr(_emailBodyWithPlaceholders, 1, posBegin – 1);
emailBody_LastPart = subStr(_emailBodyWithPlaceholders, posEnd,
newEmailBody = emailBody_FirstPart + '<BR>' + emailBody_LastPart;
}
Hope that this helps.
Kind regards,
Ana
Thanks for the response and help it works.
HI Team,
Is this extension is Free or Paid?
Hi Rudramurthy,
It’s completely Free 🤩
You can download it from https://ax.docentric.com/download-free-edition/
Cheers!
Ana