Wednesday, May 3, 2023

D365 F&O setup email notifications on workflows

 

How to setup email notifications on workflows in D365 F&O


Today we’re going to see how to manage workflow notifications in D365 Supply Chain Management.

Indeed, there are 3 methods that we will discover during 3 chapters: Via the calendar, via the action center or via e-mails.

Original post Reference

Via the calendar

The first one is the basic method, which is visible directly on the home screen.

The only condition is that you must have launched the workflow batch in « System administration \ WorkFlows \ Workflow infrastructure configuration » :



There are some HyperText links under the calendar :



Via Action Center

The second one is via the action center.

For this method a little more configuration is necessary but nothing too complicated.

Indeed in the options of the user it is necessary to go to check the box « Send notifications to Action Center » :



This has the effect of adding this type of notification:



and if we open the pane on the right we have more detail :



Note that there is sometimes a delay before the appearance of the notification.

Via e-mail

The last method is to send email notifications.

And here … it’s not the same story anymore.

3 methods of sending emails are configurable : EML, Exchange et SMTP.

Here we will focus on the SMTP method.

Let’s start by looking at what to do with user options.

there is two things to do :

·        Configure the email address that will receive the notifications. If this field is empty, notifications will not be sent:



·        Check the case « Send notifications in email » :



This is all we have to do at the user level (except that it must be done for all users who want to receive email notifications).

Now let’s go to the system settings.

The first step is to configure the sending of emails for SMTP.

To do this, go to the module « Systeme Administration \ Setup \ E-mail \ E-mail  Setup» :



In the configuration section, activate the SMTP service, set SMTP as the default provider of batch messaging and optionally limit the size of attachments.



In the « SMTP Parameter » section, contact your favorite IT specialist to obtain the server address, port, user name and password required for sending :



Once the configuration is done, it is always useful to send a test email to validate that everything is good.

The second step is to start a batch process which will take care of sending emails.

For that, go to the module « System Administration \ Periodic tasks \ Email processing \ Email distributor batch » :



Configure a frequency according to the needs of the company (every hour, 10 minutes, etc.).

We come to the 3rd part, and not the simplest, the creation of a mail template. Which is actually where we are going to define the formatting of our mail.

To create it, it will depend on the scope of the Workflow.

If the Workflow is shared between all the entities, this will be done in the module “System Administration \ Setup \ E-mail \ System email templates” :



If the Workflow is specific to the entity, this will be done in the module “Organization administration \ Setup \ Organization email templates” :



Both bring us to a similar screen in which we have to create a new line:



Email ID : the Template name, who will be choose in the workflow (next step)

Email description : free text to explain the ID (no impact)

Sender Name : name that will appear in the email as the sender

Sender Email: mandatory address but which will be replaced via SMTP

Then create a line for each of the languages used in your company like this :



Choose the language, then enter the subject. The Subject will be the title of the email.

To insert the body of the email, click on “Email message” or on “Edit”:



And there surprise! D365 asks you to load an HTML file …



A little tour on the web to find how to create an html file and here is a simple test:

Use the following link to compose your own html : https://html5-editor.net/



I’m not hiding you that I haven’t yet figured out how to find and assign the variables between the “%” and that it would be a good idea to dig a little deeper into the subject.

The result of this file is the email below:



Did you think you had finished?

Go still a little effort we are in the last straight line…

We just have to associate our Template in our workflow.

To do this, open the workflow and look at the global parameters of the workflow:



Just to clarify, if you have a header workflow that calls a line workflow, the Template can be only configured in the header workflow.

On the other hand if you configure it only at line level it will not work…

And now, your users will now be able to receive their email notifications 

I hope I didn’t give you a headache too much,

d365 F&O How to filter records in a form by code using extensions in x++

  In this case filtering vendor invoice journals by creating an extension class in the form datasource - init() method

I'm trying to filter vendor invoices with USD currency code


[ExtensionOf(formDataSourceStr(vendinvoicejournal, VendInvoiceJour))]

final class VendInvoiceJournal_FormDS_Extension

{

    public void init()

    {

        next init();

        

        str formName =  this.formRun().name();

        str callerName =  this.formRun().args().callerName();


        if(formName == 'VendInvoiceJournal' && callerName == 'VendInvoiceJournal')

        {

                          this.query().dataSourceName(this.name()).addRange(fieldnum(VendInvoiceJour, Currency)).value(SysQuery::valueLike("USD"));

            }

          

        }

    }


Tuesday, May 2, 2023

D365 FO Computed column in View ( Returning Field in View )

The technique to add a computed column begins with you adding a static method to the view. The method must return a string. The system automatically concatenates the returned string with other strings that the system generates to form an entire T-SQL create view statement.

The method that you add to the view typically has at least one call to the DictView.computedColumnString method. The parameters into the computedColumnString method include the name of a data source on the view, and one field name from that data source. The computedColumnString method returns the name of the field after qualifying the name with the alias of the associated table. For example, if the computedColumnString method is given the field name of AccountNum, it returns a string such as A.AccountNum or B.AccountNum. 

1 - Create Your View 



2- Add a Static Method to the View 
  The code below 

3 - Add Computed column 




Here is an example which illustrates how can we add computed columns to return Year , Month and substring from field in view 


private static server str ProjectID()

    {

        #define.CompView(SG_BudgetLineView)

        #define.CompDS(DimensionAttributeValueCombination)

        #define.DisplayValue(DisplayValue)


        str LedgerAccount;

        LedgerAccount =  return SysComputedColumn::returnField(tableStr(#CompView), identifierStr(#CompDS), fieldStr(#CompDS, #DisplayValue));

        return "SUBSTRING(" + LedgerAccount + ", 10 , 5)";

    }

 private static server str Year()

    {

        str yearId;

        yearId = SysComputedColumn::returnField(identifierStr("BudgetLineView"),identifierStr("BudgetTransactionLine"),identifierStr("Date"));


        return "YEAR(" + yearId + ")";

    }



   private static server str Month()
    {
        str MonthId;
        MonthId = SysComputedColumn::returnField(identifierStr("BudgetLineView"),identifierStr("BudgetTransactionLine"),identifierStr("Date"));
        return "Month(" + MonthId + ")";
        // return "mthOfYr(" + MonthId + ")";
    }

    private static server str MainAccountID()
    {
        str LedgerAccount;
        LedgerAccount = SysComputedColumn::returnField(identifierStr("SG_BudgetLineView"),identifierStr("DimensionAttributeValueCombination"),identifierStr("DisplayValue"));

        return "SUBSTRING(" + LedgerAccount + ", 0 , 9)";
    }

  Computed column; Multiplying two coulmns and returning resultant from View

private static server str compTotalCostPrice()
{
#define.CompView(SWProjForecastCost)
   #define.CompDS(ProjForecastCost)
   #define.QtyCol(Qty)
#define.PriceCol(CostPrice)


   return SysComputedColumn::multiply(
SysComputedColumn::returnField(
tableStr(#CompView),
identifierStr(#CompDS),
fieldStr(#CompDS, #PriceCol)
          ),
SysComputedColumn::returnField(
tableStr(#CompView),
identifierStr(#CompDS),
fieldStr(#CompDS, #QtyCol)
           )
        );
}

Computed column; Returning Enum Value in View
public static server str compGeneralTransType()
{
   return SysComputedColumn::returnLiteral(Transaction::ProjectInvoice);
}

Computed column; Returning Field in View
public static server str compAmount()
{
#define.CompView(SWProjForecastCost)
#define.CompDS(ProjForecastCost)
#define.CostPrice(CostPrice)


   return SysComputedColumn::returnField(tableStr(#CompView), identifierStr(#CompDS), fieldStr(#CompDS, #CostPrice));
}


Computed column; Case Statement in View
public static server str TransType()
{
#define.CompDS(ProjForecastCost)
#define.CompView(SWProjForecastCost)
   str ret;

   str ModelId = SysComputedColumn::returnField(identifierStr(SWProjForecastCost), identifierStr(ProjForecastCost), identifierStr(ModelId));

   ret = "case " + modelId +
         " when 'Sales' then 'Forecast Sales' " +
         " when 'Orders' then 'Forecast Orders' " +
         " when 'Latest' then 'Forecast Latest' " +
         " end";
   return ret;


}
Case Statement for this view looks like in SQL server as below;

   CASE T2.MODELID
      WHEN 'Sales' THEN 'Forecast Sales'
      WHEN 'Orders' THEN 'Forecast Orders'
      WHEN 'Latest' THEN 'Forecast Latest' END




Monday, May 1, 2023

D365 FO Sending Email with SSRS reports as attachment using X++

 Public class EmailCustAccountStmnt

{

public void run(CustTable _custTable)

{

SysOperationQueryDataContractInfo sysOperationQueryDataContractInfo;

SrsReportRunController reportRunController;

CustTransListContract custTransListContract;

SRSReportExecutionInfo reportExecutionInfo;

SRSPrintDestinationSettings printDestinationSettings;

SRSReportRunService srsReportRunService;

SRSProxy srsProxy;

QueryBuildRange qbrCustAccount;

QueryBuildDataSource queryBuildDataSource;

Object dataContractInfoObject;

Map reportParametersMap;

Map mapCustAccount;

MapEnumerator mapEnumerator;

Array arrayFiles;

System.Byte[] reportBytes;

Filename fileName;

Args args;

System.IO.MemoryStream memoryStream;

System.IO.MemoryStream fileStream;

CustParameters custParameters;

Email toEmail;

 

    Map                                 templateTokens;

    str                                 emailSenderName;

    str                                 emailSenderAddr;

    str                                 emailSubject;

    str                                 emailBody;

 

    Microsoft.Dynamics.AX.Framework.Reporting.Shared.ReportingService.ParameterValue[]  parameterValueArray;

 

    #define.Subject("Subject")

    #define.CustAccount("CustAccount")

    #define.EmailDate("Date");

 

    custParameters          = CustParameters::find();

 

    reportRunController     = new SrsReportRunController();

    custTransListContract   = new CustTransListContract();

    reportExecutionInfo     = new SRSReportExecutionInfo();

    srsReportRunService     = new SrsReportRunService();

    reportBytes             = new System.Byte[0]();

    args                    = new Args();

    templateTokens          = new Map(Types::String, Types::String);

    var messageBuilder      = new SysMailerMessageBuilder();

 

    custTransListContract.parmNewPage(NoYes::Yes);

 

    fileName    = strFmt("CustomerAccountStatement_%1.pdf", _custTable.AccountNum);

 

    reportRunController.parmArgs(args);

    reportRunController.parmReportName(ssrsReportStr(CustTransList, Report));

    reportRunController.parmShowDialog(false);

    reportRunController.parmLoadFromSysLastValue(false);

    reportRunController.parmReportContract().parmRdpContract(custTransListContract);

 

    // Modify query

    mapCustAccount = reportRunController.getDataContractInfoObjects();

    mapEnumerator = mapCustAccount.getEnumerator();

 

    while (mapEnumerator.moveNext())

    {

        dataContractInfoObject = mapEnumerator.currentValue();

 

        if (dataContractInfoObject is SysOperationQueryDataContractInfo)

        {

            sysOperationQueryDataContractInfo = dataContractInfoObject;

 

            queryBuildDataSource    = SysQuery::findOrCreateDataSource(sysOperationQueryDataContractInfo.parmQuery()

                                                                    , tableNum(CustTable));

            qbrCustAccount          = SysQuery::findOrCreateRange(queryBuildDataSource, fieldNum(CustTable, AccountNum));

            qbrCustAccount.value(_custTable.AccountNum);

        }

    }

 

    printDestinationSettings = reportRunController.parmReportContract().parmPrintSettings();

    printDestinationSettings.printMediumType(SRSPrintMediumType::File);

    printDestinationSettings.fileName(fileName);

    printDestinationSettings.fileFormat(SRSReportFileFormat::PDF);

 

    reportRunController.parmReportContract().parmReportServerConfig(SRSConfiguration::getDefaultServerConfiguration());

    reportRunController.parmReportContract().parmReportExecutionInfo(reportExecutionInfo);

 

    srsReportRunService.getReportDataContract(reportRunController.parmreportcontract().parmReportName());

    srsReportRunService.preRunReport(reportRunController.parmreportcontract());

 

    reportParametersMap = srsReportRunService.createParamMapFromContract(reportRunController.parmReportContract());

    parameterValueArray = SrsReportRunUtil::getParameterValueArray(reportParametersMap);

 

    srsProxy        = SRSProxy::constructWithConfiguration(reportRunController.parmReportContract().parmReportServerConfig());

    reportBytes     = srsproxy.renderReportToByteArray(reportRunController.parmreportcontract().parmreportpath()

                                                    , parameterValueArray

                                                    , printDestinationSettings.fileFormat()

                                                    , printDestinationSettings.deviceinfo());

 

    memoryStream    = new System.IO.MemoryStream(reportBytes);

    memoryStream.Position = 0;

 

    fileStream      = memoryStream;

    toEmail         = this.getCustEmail(_custTable.AccountNum);

 

    if (custParameters.EmailId && toEmail)

    {

 

        templateTokens.insert(#CustAccount, _custTable.name());

        templateTokens.insert(#EmailDate, date2StrXpp(systemDateGet()));

 

        [emailSubject, emailBody, emailSenderAddr, emailSenderName] =

            EmailCustAccountStmnt::getEmailTemplate(custParameters.EmailId, _custTable.languageId());

 

 

        messageBuilder.addTo(this.getCustEmail(_custTable.AccountNum))

                        .setSubject(strFmt("Customer account statement for %1", _custTable.AccountNum))

                        .setBody(SysEmailMessage::stringExpand(emailBody, SysEmailTable::htmlEncodeParameters(templateTokens)))

                        .addCC("");

 

        messageBuilder.setFrom(emailSenderAddr, emailSenderName);

        messageBuilder.addAttachment(fileStream, fileName);

 

        SysMailerFactory::sendNonInteractive(messageBuilder.getMessage());

 

        info(strFmt("Email sent successfully to the customer account %1", _custTable.AccountNum));

    }

    else

    {

        info(strFmt("There is no email id mappiing for this customer %1 or check the Email template setup.", _custTable.AccountNum));

    }

}

 

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];

    }

}

 

public Email getCustEmail(CustAccount _custAccount)

{

    CustTable                   custTable;

    DirPartyLocation            dirPartyLocation;

    LogisticsLocation           logisticsLocation;

    LogisticsElectronicAddress  logisticsElectronicAddress;

 

    custTable = CustTable::find(_custAccount);

 

    select firstonly Location, Party from dirPartyLocation

        where dirPartyLocation.Party                        == custTable.Party

            join RecId from logisticsLocation

                where logisticsLocation.RecId               == dirPartyLocation.Location

            join Locator from logisticsElectronicAddress

                where logisticsElectronicAddress.Location   == logisticsLocation.RecId

                    && logisticsElectronicAddress.Type      == LogisticsElectronicAddressMethodType::Email

                    && logisticsElectronicAddress.IsPrimary == NoYes::Yes;

 

    return logisticsElectronicAddress.Locator;

}