Send Email with PDF attachment of a report from SRSReportView form in AX 2012 X++

 Hello, 

I have requirement to send an Email to the customer with attachment of a PDF file of Customer Aging report. For that I have done below customization and it is quite simple process that we can apply to any other report in Ax2012.

1. Create one button in SRSReportViewer form.


2. Enable this button for only required report, in my case its CustAging report So Im enabling it for CustAging report by adding a piece of code in Init method.

         ReportName = this.controller().parmReportName();
        if (ReportName == 'CustAgingReport.DesignWithNoDetailAndNoTransactionCur')
        {
           MailReport.visible(true);
        }
3. Now add the Code under the Mail button clicked method to create and send report PDF to the customer.

Clicked:

void clicked()
{
    Microsoft.Dynamics.AX.Frameworks.Controls.ReportViewer.AxReportViewer
    axviewer;
    Microsoft.Reporting.WinForms.ReportViewer   nviewer;
    Microsoft.Reporting.WinForms.LocalReport    report;
    //ERPReportMailParameter mailParameter;
    List                                        salesReplist;
    ListIterator                                listiterator;
    str                                          Salesreps;
    str                                          reportName;
    str                                          reportPath;
    int                                          dot;
    int                                          slash;
    str                                          documentName = 'CustAgingV1';
    SRSReportName                               srsReportName;
    SRSReportDesignName                         srsReportDesignName;
    System.Exception                            ex;
    System.Array                                bytear;
    System.Object                               no;
    System.IO.File                              file;
    str                                          tmpPath;
    System.IO.FileStream                        fs;
    SysEmailParameters      emailParameters = SysEmailParameters::find();
    ;

    if(!emailParameters.AttachmentsPath)
         warning('Attachments path not defined on Email parameters');

    super();

    axviewer = AxReportViewer.control();
    nviewer = axviewer.get_ReportViewerControl();

    try
    {
        //render as PDF
        report = nviewer.get_ServerReport();
        bytear = report.Render("PDF");
        no = bytear;
        salesReplist = this.GetSalesRep();
        listiterator = new listiterator(salesReplist);
        while(listiterator.more())
        {
            Salesreps += listiterator.value();
            listiterator.next();
            if(listiterator.more())
            {
                Salesreps += ',' ;
            }

        }

        documentName =  strfmt(" CustAgeing for %1 on %2 %3",Salesreps,
                                date2str(systemDateGet(),123,2,DateSeparator::None,2,DateSeparator::None,2),
                                         time2str(timeNow(),TimeSeparator::Dot,TimeSeparator::Dot));

        //path to temp. files
        tmpPath = emailParameters.AttachmentsPath + '\\' + documentName + '.pdf'/*System.IO.Path::GetTempPath()*/;


        //save to file
        fs = System.IO.File::Create(tmpPath);
        fs.Write(no,0,bytear.get_Length());
        fs.Flush();
        fs.Close();
        if(tmpPath)
        {
            this.sendReportAsEmail(tmpPath, Salesreps);
        }

    }
    catch(Exception::CLRError)
    {
        ex = CLRInterop::getLastException();
        info(ex.ToString());
    }
}

To Send mail I have created one more method in that :

private void sendReportAsEmail(FilenameOpen    _fileName,
                                str       _SalesRep )
{
    str              SalesRep;
    ListIterator    listIteratorLoc;
    SysEmailTable   sysEmailTable;
    str              emailAddress;
    Map             mappings           = new Map(Types::String, Types::String);
    Email           custAgingEmail  = CustParameters::find().E2C_CustAgingEmailId;
    ;
    // gets the salesrep mail Ids
    listIteratorLoc = new ListIterator(this.GetSalesRep());
    while(listIteratorLoc.more())
    {
        SalesRep = listIteratorLoc.value();
        if(SalesRep)
        {
            emailAddress +=  HcmWorker::findByPersonnelNumber(SalesRep).email();
        }
        listIteratorLoc.next();

        if(listIteratorLoc.more())
        {
            emailAddress += ';' ;
        }

    }

    if (custAgingEmail)
    {
        emailAddress += strFmt('; %1',custAgingEmail);
    }
    // Build your variable/text mappings
    mappings.insert('subject', strFmt('CustomerAging report of Sales rep %1 ', _SalesRep));
    mappings.insert('Salesrep', _SalesRep);

    sysEmailTable = SysEmailTable::find('CustAgeing');
   if(sysEmailTable.RecId)
    {
        SysEmailTable::sendMail(sysEmailTable.EmailId,
                            CompanyInfo::find().LanguageId ? CompanyInfo::find().LanguageId : 'en-nz', // Chosen language
                            emailAddress, // Who you're sending the email to
                            mappings, // Your variable mappings
                            _fileName, // Location of file attachment (server/client matters) or none
                            '' , // XML if you're using XSLT
                            true, // Traceable or not?
                            curUserId(),//'admin', // Sending user
                            true); // Use retries?
        info(strFmt("Email has been sent to  %1", emailAddress));
    }
    else
    {
       warning("Email template not found");
    }
}

This has worked in Ax 2012 version. If it is D365 we need to change Send Email process.

Note: For sending attachments from Ax 2012 we must need to use the files folder location  which is specified under the EMail Parameters

Thanks,

Happy Daxing !!







joins output

 



 

https://azureintegrations.com/2020/08/23/d365-finance-and-operations-integration-using-byod/

X++ to get All Enums available in AOT

 In Standard we have SrsAnalysisEnums table which will store all the Enums used in Analytical services.Any in some cases it might not show all the Enums. To load the missing enums from the AOT we can use the following code.

To avoid any future confilcts you can create a custom table and load Enums in it.


class  PopulateSRSAnalysisEnumsBatch extends SysOperationServiceBase

{

     /// <summary>

    /// Class description

    /// </summary>

    /// <returns>ClassDescription</returns>

   public ClassDescription  classdescription()

   { 

       return "Load enum values in Enum master";

    

    }


    /// <summary>

    /// Load records in Enum master

    /// </summary>

    public void processRecords()

    {

        #AOT

        TreeNode treeNode;

        Custom_SRSAnalysisEnums analysisEnums;

        DictEnum currentEnum;

        str currentEnumName;

        int valueCount;

        RecordInsertList records = new RecordInsertList(tablenum(Custom_SRSAnalysisEnums));

        ttsbegin;

        treeNode = TreeNode::findNode(#BaseEnumsPath);

        treeNode = treeNode.AOTfirstChild();

        delete_from analysisEnums;

        while(treeNode)

        {

            currentEnum = new SysDictEnum(enumName2Id(treeNode.treeNodeName()));

            valueCount = currentEnum.values();

           

            for(int j = 0; j < valueCount; j++)

            {

                analysisEnums.EnumName      = currentEnum.name();

                analysisEnums.EnumItemName  = currentEnum.index2Symbol(j);

                analysisEnums.EnumItemValue = currentEnum.index2Value(j);

                analysisEnums.EnumItemLabel = currentEnum.index2Label(j);

                records.add(analysisEnums);

            }

            records.insertDatabase();


            treeNode = treeNode.AOTnextSibling();

        }

        ttscommit;


        Info("Data added for all AOT Enums in custom_SRSAnalysisEnums table");

       

    

    }


}

Loop through the table fields and fetch the field labels in AX X++

 To loop through all the fields available in a table we need to use the following code.It will give us the required properties specified for a field. Now im fetching the table fields labels dynamically.

Anyhow Batch job will have its own contract and controllers

/// <summary>

/// Helper class to export labels to data lake

/// </summary>

class  ExportLabelsToDataLakeBatch extends SysOperationServiceBase

{

    QueryRun                 queryRun;

    RecordInsertList         list;

    DictField               _dictField;

    DataLakeLabelsTable  dataLakeLabelsTable;

    str                      fieldLabel,edtLabel,enumLabel;

    FormDataSource           formDataSource;

    

   

   /// <summary>

    /// to call the process method

   /// </summary>

   /// <param name = "_dataContract">datacontract</param>

    public void run(ExportLabelsContract _dataContract)

    {

        

            Set                     recordSet;

            SetEnumerator           se;

            Common                  record;

            DataFeedsTableCatalog   dataFeedsTableCatalog;


            // Process records

            if (conLen(_dataContract.parmRecordCon()) > 0)

            {

                recordSet = Set::create(_dataContract.parmRecordCon());

                se = recordSet.getEnumerator();

                while (se.moveNext())

                {

                    record = se.current();


                    switch (record.TableId)

                    {

                        case tableNum(DataFeedsTableCatalog):

                        dataFeedsTableCatalog = record;

                        this.processRecord(dataFeedsTableCatalog.TableIdValue);

                            break;

                        default:

                            info(strFmt("%1", record.RecId));

                    }

                }

            }


    }


    /// <summary>

    /// Method used to prrocess the labels in every language

    /// </summary>

    /// <param name = "_tableId">Tablenumber</param>

    public void ProcessRecord(TableId _tableId)

    {

        LanguageTable   languageTable;

        list  = new RecordInsertList(tableNum(DataLakeLabelsTable));


        while select languageTable

            index hint key

        {

            this.getTablefieldsLabels(_tableId, languageTable.LanguageId);

        }

        list.insertDatabase();

    }


    /// <summary>

    /// Used to retrieve the label value of each field

    /// </summary>

    /// <param name = "_tableId">Table num</param>

    /// <param name = "_languageId">Language</param>

    public void getTablefieldsLabels(TableId _tableId, LanguageId _languageId)

    {

        DictTable   dt=new SysDictTable(_tableId);


        FieldId _fieldId=  dt.fieldNext(0);

        

        while(_fieldId)

        {

            try

            {

                ttsbegin;


                _dictField  =dt.fieldObject(_fieldId);


                if(T07_DataLakeLabelsTable::find(_dictField.tableid(), _dictField.id(),_languageId))

                {

                    _fieldId=  dt.fieldNext(_fieldId);

                    ttscommit;

                    continue;

                }


                if(_dictField.label())

                {

                    SysDictField sysdictfield = new SysDictField(_dictField.tableid(), _dictField.id());

                    fieldLabel = SysLabel::labelId2String(sysdictfield.labelLabel(),_languageId);

                    if (fieldLabel)

                    {

                        this.intilizeDatalakeTable(fieldLabel, _languageId);

                    }

                   

                }

                else

                {

                    if(_dictField.type() == Types::UserType)

                    {

                        SysDictType sysDictType = new SysDictType(_dictField.typeId());

                        edtLabel = SysLabel::labelId2String(sysDictType.labelLabel(),_languageId);

                        if(edtLabel)

                        {

                            this.intilizeDatalakeTable(edtLabel, _languageId);

                    

                        }

                       

                    }

                    else if(_dictField.type() == Types::Enum)

                    {

                        SysDictEnum sysDictEnum = new SysDictEnum(_dictField.enumId());

                        enumLabel = SysLabel::labelId2String(sysDictEnum.labelLabel(),_languageId);

                        if (enumLabel)

                        {

                            this.intilizeDatalakeTable(enumLabel, _languageId);

                        }

                

                    }

            

                }

                _fieldId=  dt.fieldNext(_fieldId);

                ttscommit;

            }

            catch(Exception::Error)

            {

                ttsabort; 

                continue;

            }

        }

    }


    /// <summary>

    /// Intilize data in DataLake custom table

    /// </summary>

    /// <param name = "_labelvale">label value</param>

    /// <param name = "_languageId">Language</param>

    public void intilizeDatalakeTable(str _labelvale, LanguageId _languageId)

    {

        dataLakeLabelsTable.FieldNumber =  _dictField.id();

        dataLakeLabelsTable.FieldName = fieldId2Name(_dictField.tableid(), _dictField.id());

        dataLakeLabelsTable.TableName  = tableId2Name(_dictField.tableid());

        dataLakeLabelsTable.Language   = _languageId;

        dataLakeLabelsTable.LabelValue = _labelvale;

        dataLakeLabelsTable.Tablenumber = _dictField.tableid();

        if(_dictField.type() == Types::UserType)

        {

            dataLakeLabelsTable.BaseType = extendedTypeId2name( _dictField.typeId());

        }

        else if(_dictField.type() == Types::Enum)

        {

            dataLakeLabelsTable.BaseType = enumId2Name(_dictField.enumId());


        }

                     

        list.add(dataLakeLabelsTable);

    }


}

Filter dynamic query data using Outerjoins in Ax / d365 fo using X++

 Sometimes we need to filter data in inquiry forms based on a particular value selected by the user.

It is very easy to filter the data by writing dynamic queries and applying ranges over the query and retrieve the required data. But if we use outer joins in our dynamic query the query range is not worked. Instead of query range we can simply use QueryFilter to get the filtered data.


As we know The major difference in between the query range and query  queryfilter will be explained by the following scenario. Query range will apply where condition just before the joins so we will get the data with joining of the result set to the other table which happened to fail the filtration in dynamic query with outer join.

QueryFilter is able to apply the where condition after the successful joining of the tables. So the range is applied to the resultset after which tends to get us the required data.

here is the sample code to filter data with outer joins in inquiry form.

filter a form with multiselection control.

1. In form Init method

public void init()

{

   

           TstCountry.visible(ProductLicenseParameters::find().TestEnableCountryFilter ? true : false);

         msCtrlCountry = SysLookupMultiSelectCtrl::construct(element, TSTCountry,                             queryStr(ProductLicenseCountryFilter));  //-------------------// staticquery

    }


2.  In Datasource Init

public void init()

{

    QueryBuildDataSource  productLicenseDs, licenseCustomerDS, custTableDs;


    super();

           productLicenseDs    = this.query().dataSourceTable(tableNum(ProductsLicenseTable));

        licenseCustomerDS   = productLicenseDs.addDataSource(tableNum(ProductLicenseCustomer));

        licenseCustomerDS.addLink(fieldNum(ProductsLicenseTable, LicenseNumber), fieldNum(ProductLicenseCustomer, LicenseNumber));

        licenseCustomerDS.joinMode(JoinMode::OuterJoin);

        custTableDs = licenseCustomerDS.addDataSource(tableNum(CustTable));

        custTableDs.addLink(fieldNum(ProductLicenseCustomer, CustAccount), fieldNum(CustTable, AccountNum));

        custTableDs.joinMode(JoinMode::OuterJoin);

        qf = this.query().addQueryFilter(custTableDs , "PartyCountry");

    }

    3. In Executequery