D365: Send email in X++ using email templates

Product:

Dynamics 365 for Finance and Operations

Purpose:

The purpose of this document is to demonstrate how we can send emails in X++ using built-in email templates. For demonstration purposes we’ll be using standard email template CnfmOrder. This email template is used to send email to customer when a sales order is confirmed.

Business requirement:

Automate sending email to customer’s contact when a sales order is confirmed.

Prerequisites:

Email parameters must be configured.

Development:

You can find built-in email templates in the system under:

Organization administration > Setup > Email templates

Note that we have keyed in the sender email field as it will be used in the code as the “Sender”.

image

You can use the following code to automatically send email using X++. The given code also handles inserting placeholder values in the email template. It uses salesId variable which is a global class member and is provided by the caller class.

public void generateEmail()
{
   // Tokens for email template replacement
   #define.SalesIdToken('salesid')
   #define.CustNameToken('customername')
   #define.DeliveryAddress('deliveryaddress')
   #define.CustomerAddress('customeraddress')
   #define.ShipDate('shipdate')
   #define.ModeOfDelivery('modeofdelivery')
   #define.Charges('charges')
   #define.Discount('discount')
   #define.Tax('tax')
   #define.Total('total')
   #define.LineProductName('lineproductname')
   #define.LineProductDesc('lineproductdescription')
   #define.LinePrice('lineprice')
   #define.LineQuantity('linequantity')
   #define.LineNetAmount('linenetamount')

   SalesTable salesTable;
   SalesLine salesLine;
   CustTable custTable;
   ContactPerson contactPerson;
   SalesTotals salesTotals;
   SysEmailId emailId;
   Map templateTokens;

   str emailSenderName;
   str emailSenderAddr;
   str emailSubject;
   str emailBody;
   str emailToAddress;

   salesTable = SalesTable::find(salesId);
   salesLine = SalesLine::find(salesTable.SalesId);
   custTable = CustTable::find(salesTable.CustAccount);
   contactPerson = ContactPerson::find(salesTable.SixCustContactPersonId);
   salesTotals = SalesTotals::construct(salesTable);
   emailToAddress = contactPerson.email();
   emailId = salesTable.SixSysEmailId;

   templateTokens = new Map(Types::String, Types::String);
   templateTokens.insert(#SalesIdToken, salesTable.SalesId);
   templateTokens.insert(#CustNameToken, custTable.name());
   templateTokens.insert(#DeliveryAddress, salesTable.deliveryAddress().Address);
   templateTokens.insert(#CustomerAddress, custTable.address());
   templateTokens.insert(#ShipDate, strFmt("%1", salesTable.ShippingDateRequested));
   templateTokens.insert(#ModeOfDelivery, strFmt("%1", salesTable.DlvMode));
   templateTokens.insert(#Charges, strFmt("", salesTotals.totalMarkup()));
   templateTokens.insert(#Discount, strFmt("", salesTotals.totalEndDisc()));
   templateTokens.insert(#Tax, strFmt("", salesTotals.totalTaxAmount()));
   templateTokens.insert(#Total, strFmt("", salesTotals.totalAmount()));
   templateTokens.insert(#LineProductName, salesLine.itemName());
   templateTokens.insert(#LineProductDesc, salesLine.itemLineDisc());
   templateTokens.insert(#LinePrice, strFmt("%1", salesLine.SalesPrice));
   templateTokens.insert(#LineQuantity, strFmt("%1", salesLine.SalesQty));
   templateTokens.insert(#LineNetAmount, strFmt("%1", salesLine.LineAmount));
      
   if (emailId)
   {
    [emailSubject, emailBody, emailSenderAddr, emailSenderName] =
      SixCustVendEmailController::getEmailTemplate(emailId, custTable.languageId());
   }
      
   var messageBuilder = new SysMailerMessageBuilder();
   messageBuilder.addTo(emailToAddress)
    .setSubject(emailSubject)
    .setBody(SysEmailMessage::stringExpand(
      emailBody, SysEmailTable::htmlEncodeParameters(templateTokens)));

   if (emailSenderAddr)
   {
    messageBuilder.setFrom(emailSenderAddr, emailSenderName);                
    SysMailerFactory::getNonInteractiveMailer().sendNonInteractive(messageBuilder.getMessage());
   }
}

The generateEmail() method calls the following method to get the email template:

protected static container getEmailTemplate(SysEmailId _emailId, LanguageId _languageId)
{
   var messageTable = SysEmailMessageTable::find(_emailId, _languageId);
   var emailTable = SysEmailTable::find(_emailId);

   if (!messageTable && emailTable)
   {
      // Try to find the email message using the default language from the email parameters
      messageTable = SysEmailMessageTable::find(_emailId, emailTable.DefaultLanguage);
   }

   if (messageTable)
   {
      return [messageTable.Subject, messageTable.Mail, emailTable.SenderAddr, emailTable.SenderName];
   }
   else
   {
      warning("@SYS135886"); // Let the user know we didn't find a template
      return ['', '', emailTable.SenderAddr, emailTable.SenderName];
   }
}

Once triggered, the code sends out the email through the configured SMTP server and can be found in the inbox:

Advertisements

D365: Create lookup in X++

Product:

Dynamics 365 for Finance and Operations

Purpose:

The purpose of this document is to demonstrate how we can create a lookup in X++ and attach it to an extension field added to the form extension of standard Sales order form. This is a good example to see how we can use event handling to achieve our goal without modifying the standard code.

Business requirement:

Display contacts of the customer in a lookup for which the sales order has been raised.

Development:

You can find the event handler below which handles the lookup event of a form control. This form control is added to the extension of SalesTable standard AX form.

[FormControlEventHandler(formControlStr(SalesTable, SixSalesOrderConfirmation_SixCustContactPersonId),
FormControlEventType::Lookup)]
public static void SixSalesOrderConfirmation_SixCustContactPersonId_OnLookup(FormControl sender, FormControlEventArgs e)
{
Query query;
QueryBuildDataSource qbdsContactPerson;
QueryBuildDataSource qbdsCustTable;
QueryBuildDataSource qbdsSalesTable;
SysTableLookup sysTableLookup;
SalesId salesId;
FormControlCancelableSuperEventArgs event;

event = e as FormControlCancelableSuperEventArgs;
salesId = sender.formRun().design().controlName(formControlStr(SalesTable, SalesTable_SalesId)).valueStr();

query = new Query();
qbdsContactPerson = query.addDataSource(tableNum(ContactPerson));

qbdsCustTable = qbdsContactPerson.addDataSource(tableNum(CustTable));
qbdsCustTable.joinMode(JoinMode::InnerJoin);
qbdsCustTable.relations(false);
qbdsCustTable.addLink(
fieldNum(ContactPerson, ContactForParty),
fieldNum(CustTable, Party));

qbdsSalesTable = qbdsCustTable.addDataSource(tableNum(SalesTable));
qbdsSalesTable.joinMode(JoinMode::InnerJoin);
qbdsSalesTable.relations(false);
qbdsSalesTable.addLink(
fieldNum(CustTable, AccountNum),
fieldNum(SalesTable, CustAccount));

qbdsSalesTable.addRange(fieldNum(SalesTable, SalesId)).value(SysQuery::value(salesId));

sysTableLookup = SysTableLookup::newParameters(tableNum(ContactPerson), sender);
sysTableLookup.addLookupfield(fieldNum(ContactPerson, ContactPersonId));
sysTableLookup.addLookupfield(fieldNum(ContactPerson, Party));
sysTableLookup.parmQuery(query);
sysTableLookup.performFormLookup();

event.CancelSuperCall();
}

D365: Reverse customer transaction in X++

Purpose:

The purpose of this document is to demonstrate how we can reverse a posted customer transaction through X++. The code below can be used as a script to automate reversal of posted customer transactions.

Product:

Dynamics 365 for Finance and Operations

Development:

Please find below the code which can be used to reverse a posted customer transaction. It will actually create and post a negative entry transaction against the transaction to reverse. Please note the code defaults reason code to “ERROR” while posting a reversal transaction. Once developed this can be triggered using the following URL:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?mi=SysClassRunner&cmp=USMF&cls=MAKCustTransReversal

Code:

class MAKCustTransReversal extends TransactionReversal_Cust
{
    public static MAKCustTransReversal construct()
    {
        return new MAKCustTransReversal();
    }

    public boolean showDialog()
    {
        return false;
    }

    public static void main(Args _args)
    {
        CustTrans custTrans;
        MAKCustTransReversal makCustTransReversal;
        ReasonTable reasonTable;
        ReasonCode reasonCode;
        ReasonRefRecID reasonRefRecID;
        InvoiceId invoiceId;
        Args args;
        ;

        invoiceId = "3392";
        reasonCode = "ERROR";        
        reasonTable = ReasonTable::find(reasonCode);
        reasonRefRecID = ReasonTableRef::createReasonTableRef(
            reasonTable.Reason, reasonTable.Description);

        custTrans = CustTrans::findFromInvoice(invoiceId);
            
        if (custTrans.RecId && !custTrans.LastSettleVoucher)
        {
            args = new Args();
            args.record(custTrans);

            makCustTransReversal = MAKCustTransReversal::construct();
            makCustTransReversal.parmReversalDate(systemDateGet());
            makCustTransReversal.parmReasonRefRecId(reasonRefRecID);
            makCustTransReversal.reversal(args);
            
            info(strFmt("%1 %2 %3 %4 reversed.",
                custTrans.Voucher,
                custTrans.TransDate,
                custTrans.Invoice,
                custTrans.Txt));
        }        
    }
}

AX 2012: General journal posting in X++

Purpose:

The purpose of this document is to describe how we can quickly post general journals (also known as GL opening balances or simply GL balances) across all the companies in X++.

This particularly comes in handy when GL balances are loaded in bulk through DIXF and the customer wants to post the loaded journals automatically as part of the DIXF load process.

Business requirement:

Ability to post GL balances across all the companies in X++ along with the posting log.

Prerequisites:

Fiscal periods are open for the relevant periods.

Assumptions:

The number sequence for the Journal batch number contains a constant segment “_GL” to designate it as a GL balance entry. Based on this assumption the given code filters GL balance record from the LedgerJournalTable.

Development:

1. Create a posting log table MAKLedgerJournalPostLog with the following fields:

untitled

where,

JournalNum        – uses LedgerJournalId EDT
Posted                  – uses NoYesId EDT
PostingLog          – uses Log EDT
TransactionTime – uses DateTimeExecuted EDT

2. Create an AOT job with the following code:

// Developed on 28 Dec 2015 by Muhammad Anas Khan
// Blog: dynamicsaxinsight.wordpress.com
// LinkedIn: pk.linkedin.com/in/muhammadanaskhan
// Description: Ability to confirm purchase order
static void MAKLedgerJournalPost(Args _args)
{
    LedgerJournalTable      ledgerJournalTable;
    LedgerJournalName       ledgerJournalName;
    LedgerJournalCheckPost  ledgerJournalValiate, ledgerJournalPost;
    Log                     errorMessage;
    SysInfologEnumerator    sysInfologEnumerator;
    MAKLedgerJournalPostLog postingLogTable;

    //Private method
    void insertLog(log _log, ledgerjournalid _journalNum, NoYes _post)
    {
        postingLogTable.clear();
        postingLogTable.PostingLog = _log;
        postingLogTable.JournalNum = _journalNum;
        postingLogTable.TransactionTime = DateTimeUtil::utcNow();
        postingLogTable.Posted = _post;
        postingLogTable.insert();
        infolog.clear();
    }

    delete_from postingLogTable;

    while select crossCompany * from ledgerJournalTable
        where ledgerJournalTable.JournalNum like '*_GL*'
            && ledgerJournalTable.Posted == NoYes::No
    {
        try
        {
            changeCompany(ledgerJournalTable.dataAreaId)
            {
                ledgerJournalName = LedgerJournalName::find(ledgerJournalTable.JournalName);
                ledgerJournalValiate = ledgerJournalCheckPost::newLedgerJournalTable(
                    ledgerJournalTable,
                    NoYes::No);
                
                ledgerJournalValiate.run();

                if (!ledgerJournalValiate.tableErrorLog())
                {
                    ledgerJournalPost = ledgerJournalCheckPost::newLedgerJournalTable(
                        ledgerJournalTable,
                        NoYes::Yes);
                    
                    ledgerJournalPost.run();
                    
                    insertLog(
                        ledgerJournalValiate.tableErrorLog(),
                        ledgerJournalTable.JournalNum,
                        NoYes::Yes);
                }
                else
                {
                    insertLog(
                        ledgerJournalValiate.tableErrorLog(),
                        ledgerJournalTable.JournalNum,
                        NoYes::No);
                }
            }
        }
        catch(Exception::Error)
        {
            sysInfologEnumerator = SysInfologEnumerator::newData(infolog.infologData());
            errorMessage = "";

            while (sysInfologEnumerator.moveNext())
            {
                errorMessage += sysInfologEnumerator.currentMessage() + "; ";
            }

            insertLog(
                errorMessage,
                ledgerJournalTable.JournalNum,
                NoYes::No);
        }
    }

    info("Posting completed. Please check log for posting results.");
}

3. After running the above AOT job, you can find the posting log by querying records in table MAKLedgerJournalPostLog from SQL server client.

AX 2012: Sales Order Posting Custom Validations

It is often a customer requirement to check for custom validations before posting a Sales Order. The best place to put your code for custom validations can be found below:

Validations for Sales Table:

AOT > Classes > SalesFormLetterProvider > checkHeading()

For example,

To check for custom field SalesTable.MAKBlocked, this is how custom validation should be added in checkHeading method. You may refer to Line# to know exactly where I have placed the code.

Untitled

Validations for Sales Line:

AOT > Classes > SalesFormLetterProvider > checkLines()

For example,

To check for custom field SalesLine.MAKBlocked, this is how custom validation should be added in checkLines method. You may refer to Line# to know exactly where I have placed the code.

Untitled

AX 2012: Using Temporary Table as Form’s Datasource

First add a method on the form to populate records in the temporary table:

public LabelsTable populateRecords()
{
    SysDictTable    dictTable = new SysDictTable(tableNum(PurchLine));
    SysDictField    dictField;
    TreeNode        treeNode;
    LabelsTable     labelsTableLocal;                 // Temporary table (InMemory)
    FieldId         fieldId = dictTable.fieldNext(0);

    while (fieldId)
    {
        dictField = dictTable.fieldObject(fieldId);

        if (dictField.isSql() && !dictField.isSystem() && dictField.name() != "Modified")
        {
            treeNode = dictField.treeNode();
            labelsTableLocal.Field = dictField.name();
            labelsTableLocal.Label = treeNode.AOTgetProperty("Label");
            labelsTableLocal.insert();
        }

        fieldId = dictTable.fieldNext(fieldId);
    }
    
    return labelsTableLocal;
}

 

Then override form’s init() method and add the following code to display the temporary table data on the form:

public void init()
{
    super();
    
    LabelsTable.setTmpData(element.populateRecords());
}

AX 2012: Loop through all the fields of a table in X++

This is how we can loop through all the fields of a table in X++:

static void Job1(Args _args)
{
    SysDictTable    dictTable = new SysDictTable(tableNum(PurchLine));
    SysDictField    dictField;
    TreeNode        treeNode;
    FieldId         fieldId = dictTable.fieldNext(0);

    while (fieldId)
    {
        dictField = dictTable.fieldObject(fieldId);

        if (dictField.isSql() && !dictField.isSystem())
        {
            treeNode = dictField.treeNode();
            info(strFmt("%1 | %2 | %3",
                dictField.name(),                                               // Field name
                treeNode.AOTgetProperty("Label"),                               // Label id
                SysLabel::labelId2String(treeNode.AOTgetProperty("Label"))));   // Label string
        }

        fieldId = dictTable.fieldNext(fieldId);
    }
}