Saturday, December 5, 2020

Purchase Requisition records filtering for specific users based on the Originator X++

Hi guys,

As you all are already aware of that Purchase Requisition list page is a shared list page across all legal entities.

"PurchReqTable" and "PurchReqLine" are the two main shared tables containing most of the information related to the Purchase Requisitions.

I got a requirement related to Purchase Requisitions visibility where in Department heads or Senior Management Personnel wants to see Purchase Requisitions of specific Originators across different legal entities that system does not support. Therefore, I had to do some minor customization in order to fulfill this requirement.

Whenever, i do any customization, i always make sure to parameterize in such a way that customization should be controlled in a proper manner and without doing any hard coding in code logic. All my customization are parameterized without affecting standard processes of the system.

Custom Table:

I created a custom shared table named "PRViewAccess" containing two fields that are sufficient to manage the customization

1) UserId  [Current user]

Datatype: String.

Set "UserId" as EDT in property

2) Worker [PR Originator]

Datatype: Int64

Set "HcmWorkerRecId" as EDT in property

Set Index property for not allowing duplicate values in the combination of "UserId" and "Worker" which means same "UserId" and "Worker" combination values can't be saved in the table and system should throw error message like "Record already exists"

Custom Form:

I created a custom form and attached "PRViewAccess" as a Datasource to the form

Created a worker lookup for selecting Past, Present or Future workers across different legal entities.

Used standard "HcmWorkerLookup" class for loading and displaying lookup in "PR Originator" field instead of customizing new lookup in the PR View Access form

Below is the code snipped of Worker lookup in the Form 

[Form]

public class PRViewAccess extends FormRun

{

    //HcmWorkerLookup class

    HcmWorkerLookup                         hcmWorkerLookup;

    public void init()

    {

        super();

        hcmWorkerLookup = HcmWorkerLookup::newActiveWorkers();

    }

    [DataSource]

    class PRViewAccess

    {

        [DataField]

        class Worker 

        {

            /// <param name = "_formReferenceControl"></param>

            /// <returns></returns>

            public Common resolveReference(FormReferenceControl _formReferenceControl)

            {

                HcmWorker ret;

                ret = super(_formReferenceControl);

                if (ret != null && !hcmWorkerLookup.validateWorker(ret.RecId))

                {

                    ret = null;

                }

                return ret;

            }

            /// <param name = "_formReferenceControl"></param>

            /// <returns></returns>

            public Common lookupReference(FormReferenceControl _formReferenceControl)

            {

                Common ret;

                ret = hcmWorkerLookup.lookupWorker(_formReferenceControl);

                return ret;

            }

        }

    }

}

Created an EventHandler class for handling Purchase Requisition records filtering for specific users based on the Originator in the "All Purchase Requisitions" List page

PR Originators or Preparers that are assigned to specific users in PRViewAccess form, those specific users will be only able to view PRs prepared by those Originators or Preparers

For example:

"PRViewAccess" table contains below records just of explanation

                       Field1       Field2

                       UserId     Worker

Record1          User1      Preparer1 

Record2          User1      Preparer2

Record3          User2      Preparer1

Record4          User3      Preparer3

When User1 will login system then this user will be able to view PRs of Preparer1 and Preparer2 only, User2 can view PRs of Preparer1 only and User3 can view PRs of Preparer3 only.

public class PRViewAccessRestrictedByPROriginators

{

    /// <summary>

    /// This class is used to restrict users by workers assigned to the users

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    [FormDataSourceEventHandler(formDataSourceStr(PurchReqTableListPage, PurchReqTable), FormDataSourceEventType::QueryExecuting)]

    public static void PurchReqTable_OnQueryExecuting(FormDataSource sender, FormDataSourceEventArgs e)

    {

        FormRun                 PurchReqTableListPage = sender.formRun();

        PRViewAccess        viewAccess;

        container                 workers;

        //curUserId method returns logged in user

        while select viewAccess

        where viewAccess.UserId == curUserId()

        {

               workers += queryValue(HcmWorker::find(viewAccess.Worker).RecId);

        }

        if(conLen(workers) != 0)

       {

                sender.query().dataSourceName(sender.name()).addRange(fieldnum(PurchReqTable, Originator)).value(con2Str(workers));

                //Applied RangeStatus as Locked to lock the range that is applied on the Originators

                sender.query().dataSourceName(sender.name()).addRange(fieldnum(PurchReqTable, Originator)).status(RangeStatus::Locked);

            }

        }

    }

}

Friday, December 4, 2020

User selection feature in "Request Change" workflow action X++

H guys,

Another blog that will be useful for those who are using workflow functionality regularly.

Background:

"Request Change" is the most commonly used workflow action. User selection feature was available in "Request Change" workflow action prior to D365 FO 10.0.8 (PU32) versions but this feature has been depreciated by Microsoft from D365 FO 10.0.8 (PU32) versions for security issue and it is not available in later versions.

As a result of this change by Microsoft, system automatically assigns workflow to the Initiator or Submitter of the workflow which fails the workflow process in those cases where in there are multiple departments involves in the same workflow approval cycle. 

Requirement:

Let's take an example of Purchase requisition workflow. Purchase requisition is being raised by IT personnel from IT department for purchasing one Laptop and submits the workflow that involves Procurement and Finance department approvals. If workflow is assigned to the Finance department after Procurement department approval and Finance department wants some amendments in Purchase requisition to be done by Procurement department. For these changes, Finance performs the "Request Change" workflow action on which they weren't able to select the Procurement user and workflow is assigned to IT user who is not aware of the changes in the Purchase requisition then it will break the whole workflow approval cycle and ultimately it will disturb business operations. I came across similar kind of requirement where user selection feature was required as this feature was being used so often by users and process owners were not at all accepting this solution that's why i had to make changes in the core functionality of workflow in order to fulfill the business requirements.

Solution:

Below is the code snippet of the solution that is working absolutely fine. I hope this will be helpful for you if you have the same requirements as i had.

Create two new Extension classes

Below class will make user selection feature visible in Request Change dialog form

[ExtensionOf(formStr(WorkflowWorkItemActionDialog))]

final class WorkflowWorkItemActionDialog_Extension

{

    public void run()

    {

        next run();

        switch (workflowWorkItemActionDialog.parmWorkItemActionType())

        {

            case WorkflowWorkItemActionType::RequestChange:

                groupUser.visible(true);

        }       

    }

}

Below class will override the request change process by replacing workflow submitted user with the user selected in the user drop down in Request change dialog form

[ExtensionOf(classStr(WorkflowWorkItemActionManager))]

final class WorkflowWorkItemActionManager_Extension

{

    public static void dispatchWorkItemAction(  WorkflowWorkItemTable _workItem,

                                                WorkflowComment _comment,

                                                WorkflowUser _toUser,

                                                WorkflowWorkItemActionType _workItemActionType,

                                                menuItemName _menuItemName,

                                                Name _queueName)

    {

        #Workflow

        SysWorkflowTable workflowTable;

        SysDictWorkflowElement dictElement;

        if (_workItemActionType == WorkflowWorkItemActionType::RequestChange)

        {

            _workItemActionType =  WorkflowWorkItemActionType::Deny;

        }

        next dispatchWorkItemAction(_workItem,

                                            _comment,

                                            _toUser,

                                            _workItemActionType,

                                            _menuItemName,

                                            _queueName);

        

    }

}

Thursday, December 3, 2020

Save D365 FO cloud environment Document print pdf file in Sharepoint location through X++ batch job

Hi folks,

Here is another blog that will be helpful in case where Dynamics 365 Finance and Operations cloud environment Document print pdf file needs to be saved to Sharepoint location that can be shared with any other external application for integration purpose. 

I am using Sales Quotations print pdf as an example in this post. You can follow the same practice for Purchase order, Sales Invoices or any other Document print pdf as per your requirement.

Create a class that should be inherited from RunBaseBatch framework class in order to execute batch jobs

Below is the code snippet of batch job that will save Document print pdf file in Sharepoint location on periodic basis from Dynamics 365 Finance and Operations cloud environment. 

class SalesQuotationsIntegration extends RunBaseBatch

{

}

Below method can be used to save multiple Document print pdf file in Sharepoint location. Create a shared folder in Sharepoint site where this batch job will save pdf files. I created SalesQuotations as a shared folder where files will be kept. 

Using Try and Catch block to handle any exceptions in batch job error log message.

public static void PublishQuotations(QuotationId _quotationId)

{

        try

        {

            SrsReportRunController                      controller = new SrsReportRunController();

            SalesQuotationContract                      contract = new SalesQuotationContract();

            SRSPrintDestinationSettings                 settings;

            Array                                       arrayFiles;

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

            SRSProxy                                    srsProxy;

            SRSReportRunService                         srsReportRunService = new SrsReportRunService();

            CUSTQUOTATIONCONFIRMSALESLINK               custQuotationConfirmSalesLink;

            CustQuotationJour                           custQuotationJour;

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

            Map                                         reportParametersMap;

            SRSReportExecutionInfo executionInfo = new SRSReportExecutionInfo();

            Args                                    args;

            args = new Args();

            controller.parmArgs(args);

            controller.parmReportName(ssrsReportStr(SalesQuotation, Report));

            controller.parmExecutionMode(SysOperationExecutionMode::Synchronous);

            controller.parmShowDialog(false);

            controller.parmLoadFromSysLastValue(false);

            select firstonly RecId from custQuotationJour

            where  custQuotationJour.QuotationDocNum == _quotationId;            

            contract.parmRecordId(custQuotationJour.RecId);

            contract.parmDocumentTitle("Sales Quotation");

            controller.parmReportContract().parmRdpContract(contract);

            settings = controller.parmReportContract().parmPrintSettings();     

            settings.printMediumType(SRSPrintMediumType::File);

            settings.fileFormat(SRSReportFileFormat::PDF);

            settings.overwriteFile(true);

            settings.fileName(_quotationId);

            controller.startOperation();                                         controller.parmReportContract().parmReportServerConfig(SRSConfiguration::getDefaultServerConfiguration());

            controller.parmReportContract().parmReportExecutionInfo(executionInfo);

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

            srsReportRunService.preRunReport(controller.parmreportcontract());

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

            parameterValueArray = SrsReportRunUtil::getParameterValueArray(reportParametersMap);


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

            reportBytes = srsproxy.renderReportToByteArray(controller.parmreportcontract().parmreportpath(),

                                                      parameterValueArray,

                                                      settings.fileFormat(),

                                                      settings.deviceinfo());


            System.UriBuilder builder = new System.UriBuilder("https://xxxxx.sharepoint.com");

            str extId = xUserInfo::getCurrentUserExternalId();

            Microsoft.Dynamics.AX.Framework.FileManagement.SharePointDocumentStorageProvider provider;

            Microsoft.Dynamics.AX.Framework.FileManagement.DocumentLocation documentLocation = new Microsoft.Dynamics.AX.Framework.FileManagement.DocumentLocation();

            provider = new Microsoft.Dynamics.AX.Framework.FileManagement.SharePointDocumentStorageProvider("xxxxx.sharepoint.com", "sites/SalesQuotations","Shared%20Documents/SalesQuotations", extId);

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

            documentLocation = provider.SaveFile(newGuid(), _quotationId+ ".pdf",         System.Web.MimeMapping::GetMimeMapping(_quotationId + ".pdf"), mstream);

        }

        catch(Exception::Error)

        {

            error("File not saved successfully in shared location");

        }

}


Applying range on the lookup based on the selection of value on other field X++

Hi folks,


Since, I have been working on Dynamics 365 Finance and Operations development for a while. I would like to share some important tips and tricks related to D365 FO development.

I had been given a scenario where I needed to show a filtered value based on the selection of value on the other field.

Since, you guys are already familiar on how to create lookups in Dynamics AX 2012 by overriding lookup method. In D365, there is a slight change in creating lookups in standard objects because overlayering is no more allowed.

You need to first create form extension class. 
[ExtensionOf(formStr(MarkupTrans))]
final class MarkupTransForm_Extension
{
}

Copy the event handler method signature, paste it in your extension class and then write the logic below:
    /// <summary>
    /// Added a custom field "Charges categories" in MarkupTable which is used for setting Charges codes. Lookup values of Charges codes will be populated based on the selection of Charges categories that are setup in Charges code setup form.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormControlEventHandler(formControlStr(MarkupTrans, MarkupTrans_MarkupCode), FormControlEventType::Lookup)]
    public static void MarkupTrans_MarkupCode_OnLookup(FormControl sender, FormControlEventArgs e)
    {
        FormRun                              element;
        FormControl                         formCtrl;
        Query                                   query;
        QueryBuildDataSource        markupTrans;
        SysTableLookup                  sysTableLookup;
        
        element  = sender.formRun();
        formCtrl = element.design().controlName(formControlStr(MarkupTrans,        ChargesCategory));

        sysTableLookup = SysTableLookup::newParameters(tableNum(MarkupTable), sender);
        
        query = new Query();
        markupTrans = query.addDataSource(tableNum(MarkupTable));
        sysTableLookup.addLookupfield(fieldNum(MarkupTable, MarkupCode));

        //below code is used to get the filtered lookup based on the other field value.
        markupTrans.addRange(fieldNum(MarkupTable, ChargesCategory)).value(formCtrl.valueStr());

        sysTableLookup.parmQuery(query);
        sysTableLookup.performFormLookup();

        FormControlCancelableSuperEventArgs event = e as FormControlCancelableSuperEventArgs;
        event.CancelSuperCall();
    }

Tuesday, December 1, 2020

Get Delivery Notes or Packing Slip IDs against Sales invoice X++

Hi folks,

Recently, i had a requirement where Delivery Notes or Packing Slip IDs were required to be displayed in the Sales invoice print out. Since, there are multiple Packing Slips against the same Sales Invoice and there is no direct relation between Sales invoice related tables and Packing slip related so i had to scratch my head and explored relations in Inventory transactions related tables because in both sales invoice and packing slip transactions, inventory transactions are impacted. Finally, i was able to find a way to find Delivery notes against Sales invoice.

Below is the code snippets of how you can get Delivery Notes or Packing Slip IDs against a Sales Invoice and prints on the Sales invoice print out

int                  i = 0;

str                  packingSlipIds;

while select custInvoiceTransPS

where  custInvoiceTransPS.SalesId       == _custInvoiceJour.SalesId

&&     custInvoiceTransPS.InvoiceId     == _custInvoiceJour.InvoiceId

&&     custInvoiceTransPS.InvoiceDate   == _custInvoiceJour.InvoiceDate

join   custPackingSlipTrans group by custPackingSlipTrans.PackingSlipId

where  custPackingSlipTrans.InventTransId == custInvoiceTransPS.InventTransId

exists join inventTransOrigin

where  inventTransOrigin.InventTransId    == custPackingSlipTrans.InventTransId

exists join inventTrans

where inventTrans.InventTransOrigin == inventTransOrigin.RecId

&&    inventTrans.PackingSlipId     == custPackingSlipTrans.PackingSlipId

&&    inventTrans.InvoiceId         == _custInvoiceJour.InvoiceId

{

      if(i == 0)

     {

           packingSlipIds = custPackingSlipTrans.PackingSlipId;

           salesInvoiceTmp.DeliveryNotes = packingSlipIds;

     }

     else

    {

           packingSlipIds = packingSlipIds + ", " + custPackingSlipTrans.PackingSlipId;

           salesInvoiceTmp.DeliveryNotes = packingSlipIds;

     }

           i++;

}

Get VAT registration number for Customer, Vendor and Legal entity X++

Hi D365 FO Tech Guys,

Greetings!

This is my first technical blog of D365 FO. I want to share my experience with D365 FO tech guys whatever i have learnt so far because i have a strong belief that the more you share knowledge with others the more you can contribute in the community and this is my objective to help people who wants to pursue their career in Dynamics 365 Finance and Operations technical side.

In this post, i am going to share X++ code snippets of how you can get VAT number for Customer, Vendor and Legal entity. This requirement is very common in those countries where VAT regulations are imposed and VAT number is mandatory to be displayed on external document prints like Sales invoice, Purchase order etc.

Since, there is a possibility of multiple VAT registration number versions for the same vendor, customer or legal entity and one version is active that's why ValidTo condition in the X++ query is required to handle multiple VAT registration number versions and capture active VAT registration number version

Customer VAT:

select custTable

where custTable.AccountNum == "CUST00001"

join  dirpartytableCust

where dirpartytableCust.RecId==custTable.Party

join  dirpartylocationCust

where dirpartylocationCust.Party==dirpartytableCust.RecId && dirpartylocationCust.IsPrimary==1

join  RegistrationNumber from taxregistrationCust

where taxregistrationCust.DirPartyLocation == dirPartyLocationCust.RecId && taxregistrationCust.ValidTo>=today();

info(strfmt("Customer VAT: %1", taxregistrationCust.RegistrationNumber));


Vendor VAT:

select vendTable

where vendTable.AccountNum == "VEND00001"

join  dirpartytableVend

where dirpartytableVend.RecId==vendTable.Party

join  dirpartylocationVend

where dirpartylocationVend.Party==dirpartytableVend.RecId && dirpartylocationVend.IsPrimary==1

join  RegistrationNumber from taxregistrationVend

where taxregistrationVend.DirPartyLocation == dirPartyLocationVend.RecId && taxregistrationVend.ValidTo>=today();

info(strfmt("Vendor VAT: %1", taxregistrationVend.RegistrationNumber));


Legal entity VAT:

select _companyInfo

where _companyInfo.DataArea ==curExt()

join  dirpartytable

where dirpartytable.PartyNumber ==_companyInfo.PartyNumber

join dirpartylocation

where dirpartylocation.Party == dirpartytable.RecId && dirpartylocation.IsPrimary==1

join RegistrationNumber from taxregistration

where taxregistration.DirPartyLocation == dirPartyLocation.RecId && taxregistration.ValidTo>=today();

info(strfmt("Company VAT: %1", taxregistration.RegistrationNumber));

Enable and Disable Vendor invoice button on PO list and details page X++

Hi folks, This blog will be useful where vendor invoice process is required to be controlled based on certain conditions. Requirements: Proc...