Monday, February 24, 2020

D365FO - Automatic build in azure DevOps

The build VM contains the build agent which was released as part of TFS 2015. When you deploy the Build VM, the build agent is configured by default to connect and sync with the Azure DevOps project. As a part of the Build VM configuration, the default build definition is also created and configured, as shown below.
Build1
Default build definition contains multiple tasks to perform specific operation, as described below.
  1. Configure the predefined variables parameters that will be passed to the build. To set up a clean database for every build execution, provide the name of the database backup file for the DatabaseBackupToRestore variable. The packages folder is restored at every build with a copy of a clean package folder.
    build2
  2. Build the solution to discover and build all modules under “Trunk/Main” branch as shown below.
    Build3
  3. Use “Deploy Report” task to generate reports and deploy on build VM.
  4. Use “Database Sync” task to synchronize the database to local SQL on build VM.
  5. After the build is successful, create a deployable package that can be used to update sandbox/ staging environment.
    Build4
  6. “Copy and publish build artifacts” uploads the deployable package to Azure DevOps artifacts location.
    build5
  7. For test execution, there are three default tasks “Test Setup”, “Execute Test” and “Test End”.
    build7
  8. The default build is scheduled to trigger start every day at 5 P.M. You can change trigger as per your team’s need to “Continuous” for each check-in.
    build8
You can make changes to the default configuration, and the build VM will be ready to trigger a build.

Start a build and verify the build and test execution results

After you review the default build configuration, you can manually trigger a build from Visual Studio IDE or Azure DevOps web interface.
  1. Open your browser and connect to the Azure DevOps URL.
  2. Login using your credentials.
  3. On the home page, under Recent projects and solutions, select a project.
  4. From top links options, select BUILD.
  5. On the left panel, select the default build definition instance.
  6. Right-click and select Queue Build to trigger a build for your module and test module that is already checked into the Azure DevOps source control.
image045
Success or failure for the build will display, as shown by the following examples. View all builds.
build9
Select specific completed build and view success/ failure details.
build10 Click on Test link to visualize test execution failure.
build11

Monday, February 17, 2020

D365FO - WebAPI registration for custom web services


When developing web services within D365FO for other applications/languages we need to follow a new authentication process compared to AX 2012 since everything is hosted on Azure. The following will show you the settings that need to be defined within your Azure dashboard and within D365FO to enabled authentication to execute a custom web service (SOAP/JSON) or Odata calls.

It is good to note that you may need to setup up a webapi or a native application depending on what you are trying to accomplish. Both are about the same just the native application does not require the key generation.

Before I go over the steps needed the following goes over the multiple types of authentication for Azure and explains the difference between Native vs Web API auth scenarios: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios 


Azure Setup for defined credentials/web api

Register a web app / api

Step 1: In the Azure port go to Azure Active Directory > App registrations > New Application registration

Step 2: Enter in the environment information for D365 and hit create

Step 3: Once initialized click on the settings button


Step 4: Click on required permissions > add > Select API


Step 5: Choose "Microsoft Dynamics ERP"

Select which options you want to give it access to. (usually all of them)

Choose done.

Step 6:  choose "keys"

 Under password input a description and choose when the cert should expire






Hit the save button and save the "value" aka the key. This is what will be used as a "handshake"


Step 7 (optional): Do the same thing but for a native application (interactive login). Just enter the main login url as the redirect url. You will not need the Key generation part.



D365FO setup:

Step 8 : In D365FO go to System administration > Setup > Azure Active Directory applications and create a new record for the app registration with the client id(s) from the Azure setup. If you are using the defined cred's via the web app / api type as listed below the User Id listed in this screen is what the system will log an new records created via the webservice




D365FO Web API registration for POST MAN

Most of the time we used 3rd Party tool like fiddler, Post Man and SoupUI as client to consume web services.


In this blog i will explain, How can we use POST MAN to consume Web-API by using Oauth2 Azure Authentication.


Step-1 Download the POST MAN from the this link.

Step-2 Register The Azure Application (Web API). You can follow this link

Step-3 Add the below URL in Reply URL section https://www.getpostman.com/oauth2/callbackcheck the below screen shot.



Step-4 Once App registration completed Enter Application ID in D365FO

Reference screenshot

Step-5 Open POST man and navigate to Authorization Tab and select OAuth2 as Authentication Type.

Reference screenshot


Step-6 click on Get New Access Token. A Popup will appear.

Reference screenshot


Step-7 Fill the required fields Like below

Call Back URL > You can't change this.
Token Name (As per your requirement)
Auth URL

https://login.windows.net/YourTenant.com/oauth2/authorize?resource=https://EnvironmentURL.operations.dynamics.com

Access Token URL
https://login.windows.net/YourTenant.com/oauth2/token?resource=https://EnvironmentURL.operations.dynamics.com

You can find the tenant from AX as well. Open D365FO Click on Setting icon. you can find the on top right of the screen then click on about.




Client ID > Application ID You Registered On Azure Portal
Secret Key > Enter the secret key you generate against your Azure Application.
If you don't provide secret key then a popup window will appear for login.


Reference Screenshot

Step-8 Click on Request Toke. Within 4 to 5 second you will get the Azure Authentication Token.

Step-9  Now select the Header value from the drop down and click on USE token to add this token in your request.

Reference screenshot


Step-10 Navigate to Header Tabs for the verification of Authorization token in your request

Reference screenshot

Step-11 Enter the Complete service URL and click on send. in my case I have called getFoo service.

Reference screenshot


Please check the above screen shot. You will find the success result 

Original post: http://d365technext.blogspot.com/2018/07/dynamics-365-finance-operation-webapi.html

Thursday, February 13, 2020

Pre and Post Event Handlers in D365FO

Ref

http://dynamicsaxaptablog.blogspot.ae/2016/03/event-handlers-and-prepost-methods-ax7.html

https://devserra.wordpress.com/tag/ax7/

https://stoneridgesoftware.com/event-handler-methods-in-ax7/

https://shyamkannadasan.blogspot.com/2016/09/pre-and-post-event-handlers-in-ax7.html

i.e.

Set Default Value in Custom Check box

In Table Datasource Events --> right click on Init Value--> Copy Post Event Handlers

Create new Class and below code

Table Method Event Handler

class test_PurchTableEventHanlders
{
    /// <summary>
    ///
    /// </summary>
    /// <param name="args"></param>
    [PostHandlerFor(tableStr(PurchTable), tableMethodStr(PurchTable, initValue))]
    public static void PurchTable_Post_initValue(XppPrePostArgs args)
    {
        PurchTable purchTable = args.getThis() as PurchTable;
        purchTable.test_PrintNote=NoYes::Yes;
    }

}

Form Event Handler - CustTableFormEventHandler

class CustTableFormEventHandler
{
    [FormDataSourceEventHandler(formDataSourceStr(CustTable, CustTable), FormDataSourceEventType::Activated)]
    public static void CustTable_OnActivated(FormDataSource sender, FormDataSourceEventArgs e)
    {
        CustTable           custTable     = sender.cursor();
        FormDataSource      custTable_ds  = sender.formRun().dataSource("CustTable");
        FormRun             element       = sender.formRun();
        FormControl         myNewButton   = element.design(0).controlName("MyNewButton");

        myNewButton.enabled(false);
    }

}


Table Events Event Handler

/// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [DataEventHandler(tableStr(CustInvoiceTrans), DataEventType::Inserted)]
    public static void CustInvoiceTrans_onInserted(Common sender, DataEventArgs e)
    {
        ValidateEventArgs   event       = e as ValidateEventArgs;
        CustInvoiceTrans custInvoiceTrans=sender as CustInvoiceTrans;

        SalesTable salesTable=SalesTable::find(custInvoiceTrans.salesid);
        if(salesTable.SalesStatus==SalesStatus::Invoiced )
            {
       
         //code
            }

    }

Table Method's Post Event Handler (Table - SalesTable, method - updateBackStatus)

 /// <summary>
    ///
    /// </summary>
    /// <param name="args"></param>
    [PostHandlerFor(tableStr(SalesTable), tableMethodStr(SalesTable, updateBackStatus))]
    public static void SalesTable_Post_updateBackStatus(XppPrePostArgs args)
    {
        SalesTable salesTable=args.getThis();
 
        SalesTable salesTable=SalesTable::find(custInvoiceTrans.salesid);
        if(salesTable.SalesStatus==SalesStatus::Invoiced )
           {
                      //code
                }
    }

Form Control Event Handlers

https://stoneridgesoftware.com/event-handler-methods-in-ax7/

// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormControlEventHandler(formControlStr(SalesEditLines, OK), FormControlEventType::Clicked)]
    public static void OK_OnClicked(FormControl sender, FormControlEventArgs e)
    {
   
        FormCommandButtonControl  callerButton = sender as FormCommandButtonControl;  //Retrieves the button that we're reacting to
        FormRun form = callerButton.formRun(); //Gets the running SalesEditLines form


        //Get the salesId that was selected in the SalesEditLines form
        FormDataSource salesParmTable_ds = form.dataSource(formDataSourceStr(SalesEditLines, SalesParmTable)) as FormDataSource;
        SalesParmTable salesParmTable = salesParmTable_ds.cursor();

     

        SalesTable salesTable=salesParmTable.salesTable();

        if(salesTable.SalesStatus==SalesStatus::Invoiced)
        {
         //code

        }

    }

OnModified Event for String Control in Sales Table Form

/// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormControlEventHandler(formControlStr(SalesTable, Reason), FormControlEventType::Modified)]
    public static void Reason_OnModified(FormControl sender, FormControlEventArgs e)
    {
//Retrieves the string control that we're reacting to
        FormStringControl   str_Reason = sender as FormStringControl ;
        FormRun form = strReason.formRun();

OR
//Retrieves the string control that we're reacting to
        // FormControl Reason = element.design(0).controlName("Reason");


     
        //Get the salesId that was selected in the SalesEditLines form
        FormDataSource salesLine_ds = form.dataSource(formDataSourceStr(SalesTable, SalesLine)) as FormDataSource;
        SalesLine  salesline = salesLine_ds.cursor();
               
// do your logic

    }

Form Events Event Handler

Form SalesEditLines --> Events -->OnClosing

    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormEventHandler(formStr(SalesEditLines), FormEventType::Closing)]
    public static void SalesEditLines_OnClosing(xFormRun sender, FormEventArgs e)
    {
 
            //Retrieves the button that we're reacting to
        FormDataSource salesParmTable_ds= sender.dataSource("SalesParmTable"); //Gets the running SalesEditLines form

        //Get the salesId that was selected in the SalesEditLines form

        SalesParmTable salesParmTable = salesParmTable_ds.cursor();


        SalesTable salesTable=salesParmTable.salesTable();

            SalesId salesid=salesTable.SalesId;
            if(salesTable.SalesStatus==SalesStatus::Invoiced )
            {
//Any Code
            }

    }


Enable and Disable Form Control in Form Init Method in Form Extension Class based on Purch Parameters

[ExtensionOf(formStr(PurchTable))]
public final class S_PurchTableForm_Extension
{
 
    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormEventHandler(formStr(PurchTable), FormEventType::Initialized)]
    public static void PurchTable_OnInitialized(xFormRun sender, FormEventArgs e)
    {
        FormRun element = sender;
        FormControl S_SubPriceFieldsGrid = element.design(0).controlName("S_SubPriceFieldsGrid");
        S_SubPriceFieldsGrid.visible(PurchParameters::find().S_PriceFields); //
     
    }

}

Enable ReceiptDateRequested and ShippingDateRequested based on Sales Origin Id in Sales Header

If Sales Origin ID is EDI Then disable above both fields

Sales Table Form --> Events --> OnActivated

 ////</summary>
     ////<param name="sender"></param>
     ////<param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesTable), FormDataSourceEventType::Activated)]
    public static void SalesTable_OnActivated(FormDataSource sender, FormDataSourceEventArgs e)
    {
        boolean     allowEdit = true;

     
        SalesTable           SalesTable     = sender.cursor();
        FormDataSource      salesTable_ds  = sender.formRun().dataSource("SalesTable");

        FormRun             element       = sender.formRun();
        FormControl         ReceiptDateRequested   = element.design(0).controlName("Delivery_ReceiptDateRequested");
        FormControl         ShippingDateRequested   = element.design(0).controlName("Delivery_ShippingDateRequested");
        FormControl         SalesOriginId   = element.design(0).controlName("Administration_SalesOriginId");

        if (SalesTable.SalesOriginId == "EDI")
        allowEdit = false;

        ShippingDateRequested.enabled(allowEdit);
        ReceiptDateRequested.enabled(allowEdit);
        SalesOriginId.enabled(allowEdit);

    }


Enable and Disable based on Sales Id Field value modifications.

Sales Table Form --> Data Sources --> Sales Table --> SalesOriginId --> Events --> OnModified

   ////<summary>

     ////</summary>
     ////<param name="sender"></param>
     ////<param name="e"></param>
    [FormDataFieldEventHandler(formDataFieldStr(SalesTable, SalesTable, SalesOriginId), FormDataFieldEventType::Modified)]
    public static void SalesOriginId_OnModified(FormDataObject sender, FormDataFieldEventArgs e)
    {

        boolean     allowEdit = true;
     
        FormDataSource      salesTable_ds  = sender.dataSource();
        SalesOriginId salesOriginId = sender.getValue(0);
   
        if (salesOriginId == "EDI")
        allowEdit = false;

        salesTable_ds.object(fieldNum(SalesTable, ReceiptDateRequested)).allowEdit(allowEdit);
        salesTable_ds.object(fieldNum(SalesTable, ShippingDateRequested)).allowEdit(allowEdit);
        sender.allowEdit(allowEdit);
   
    }


Reference


Post Event handler for Table CustInvoiceTrans and its method InitFromSalesLine


 /// <summary>
    ///
    /// </summary>
    /// <param name="args"></param>
    [PostHandlerFor(tableStr(CustInvoiceTrans), tableMethodStr(CustInvoiceTrans, initFromSalesLine))]
    public static void CustInvoiceTrans_Post_initFromSalesLine(XppPrePostArgs args)
    {
        CustInvoiceTrans        custInvoiceTrans    = args.getThis();
        SalesLine               salesLine           = args.getArg('salesLine');
        SalesParmLine _salesParmLine=  args.getArg('_salesParmLine');
     
      // Qty Calculation while creating Invoice
        custInvoiceTrans.Custommethodcalculate( _salesParmLine.DeliverNow );

          }


Getting Custom field value from Warehouse to PO Header when creating New PO.

POST Event Handler for PurchTable OnModifiedField Event.

  ////<summary>

     ////</summary>
     ////<param name="sender"></param>
     ////<param name="e"></param>
    [DataEventHandler(tableStr(PurchTable), DataEventType::ModifiedField)]
    public static void PurchTable_onModifiedField(Common sender, DataEventArgs e)
    {
        ModifyFieldEventArgs event          = e as  DataEventArgs;
        PurchTable           purchTable     = sender as PurchTable;
        FieldId              fieldid        = event.parmFieldId();

        switch(fieldid)
        {
            case fieldNum(PurchTable, InventLocationId):
         
               purchTable.aaId =  InventLocation::find(purchTable.InventLocationId).aacId;
                break;
        }

    }


Cost Price Validation while posting stock journals.

 [PostHandlerFor(tableStr(InventJournalTrans), tableMethodStr(InventJournalTrans, checkAmount))]
    public static void InventJournalTrans_Post_checkAmount(XppPrePostArgs args)
    {
        boolean ret = args.getReturnValue();
        InventJournalTrans  inventJournalTrans = args.getThis() as InventJournalTrans;
        if (InventJournalName::find(inventJournalTrans.Qty > 0 && inventJournalTrans.CostPrice == 0)
        {
            ret = checkFailed(strFmt("Cost Price is 0 for Line %1",inventJournalTrans.LineNum));
        }
        args.setReturnValue(ret);
    }

Form Sales Update Remain --> CloseOK Post Button

 [PostHandlerFor(formStr(SalesUpdateRemain), formMethodStr(SalesUpdateRemain, closeOk))]
    public static void SalesUpdateRemain_Post_closeOk(XppPrePostArgs args)
    {
        FormRun element = args.getThis();
        FormRun callerForm = element.args().caller();
        FormDataSource formDataSource = element.ObjectSet();
        SalesLine salesLine;
        FormRealControl   remainInventPhysical = element.design(0).controlName("RemainInventPhysical");

        if (callerForm && element.args().caller().name() == formStr(SalesTable))
        {
            salesLine = callerForm.dataSource(2).cursor() as SalesLine;
            //do your logic
        }


    }

Add new value for custom field in sales line via Sales Order Lines Data Entity (Pre Insert Entity )

 [PreHandlerFor(tableStr(SalesOrderLineEntity), tableMethodStr(SalesOrderLineEntity, insertEntityDataSource))]
    public static void SalesOrderLineEntity_Pre_insertEntityDataSource(XppPrePostArgs args)
    {
        DataEntityDataSourceRuntimeContext dataSourceCtx=args.getArgNum(2);
         
        switch (dataSourceCtx.name())
        {
            case dataEntityDataSourceStr(SalesOrderLineEntity, SalesLine):
                SalesLine salesLine = dataSourceCtx.getBuffer();
                salesLine.R_RefrigerationType  =  InventTable::find(salesLine.ItemId).R_RefrigerationType;
           
                break;
        }
    }


Validate Field

[PostHandlerFor(tableStr(VendTable), tableMethodStr(VendTable, validateField))]
    public static void VendTable_Post_validateField(XppPrePostArgs _args)
    {
        VendTable   vendTable   = _args.getThis();
        boolean     returnValue = _args.getReturnValue();

        _args.setReturnValue(vendTable.S_validateField(_args.getArg('p1')) && returnValue);

    }

 /// <param name="_fieldId">
    /// Field identifier
    /// </param>
    /// <returns>
    /// True if field validation is ok otherwise false
    /// </returns>
    public boolean S_validateField(FieldId _fieldId)
    {
        switch(_fieldId)
        {
          
            case fieldNum(VendTable, S_IsM):
            if (!this.S_IsManufacturer)
                {
                    return this.S_validateFieldM();
                }
                break;
        }

        return true;

    }

  // external item check
    private boolean S_validateFieldM()
    {
        boolean                     ret = true;
        S_VendExternalItem       vendExternalItem;
        CustVendExternalItem        custVendExternalItem;

        select firstOnly vendExternalItem
        where   vendExternalItem.VendAccount    == this.AccountNum;

        if (vendExternalItem)
        {
            select custVendExternalItem where custVendExternalItem.RecId == vendExternalItem.RefRecId;

            ret = checkFailed(strFmt("test", vendExternalItem.VendAccount, custVendExternalItem.ItemId));
        }

        return ret;

    }

Pre Event Handler 

[PreHandlerFor(tableStr(PurchLineHistory), tableMethodStr(PurchLineHistory, update))]
    public static void PurchLineHistory_Pre_update(XppPrePostArgs args)
    {
        PurchLine _purchline;
        PurchLineHistory _purchLineHistory= args.getThis();

        //select   _purchline where _purchline.InventRefId ==_purchLineHistory.InventRefId
        //     && _purchline.ItemId ==_purchLineHistory.ItemId ;

        //_purchline =PurchLine::findInventTransId(_purchLineHistory.InventRefId);

        _purchline=PurchLine::find(_purchLineHistory.PurchId,_purchLineHistory.LineNumber );

            _purchLineHistory.PriceDiscTableRefRecId =_purchline.PriceDiscTableRefRecId;
     


    }

FormDataSourceEventHandler

 /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    ///// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, BUSInventTable), FormDataSourceEventType::Creating)]
    public static void  BUSInventTable_OnCreating(FormDataSource sender, FormDataSourceEventArgs e)
    {
      
        FormRun             element1       = sender.formRun();
        element1.numberSeqFormHandler().formMethodDataSourceCreatePre();
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, BUSInventTable), FormDataSourceEventType::Created)]
    public static void BUSInventTable_OnCreated(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormRun             element1       = sender.formRun();
        element1.numberSeqFormHandler().formMethodDataSourceCreate();
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, BUSInventTable), FormDataSourceEventType::Deleting)]
    public static void BUSInventTable_OnDeleting(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormRun             element1       = sender.formRun();
        element1.numberSeqFormHandler().formMethodDataSourceDelete();
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, BUSInventTable), FormDataSourceEventType::Written)]
    public static void BUSInventTable_OnWritten(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormRun             element1       = sender.formRun();
        element1.numberSeqFormHandler().formMethodDataSourceWrite();
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, BUSInventTable), FormDataSourceEventType::ValidatingWrite)]
    public static void BUSInventTable_OnValidatingWrite(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormRun             element1       = sender.formRun();
     //   element1.numberSeqFormHandler().formMethodDataSourceWrite();
        boolean ret = true;
        ret = element1.numberSeqFormHandler().formMethodDataSourceValidateWrite();

    }