Wednesday, August 28, 2019

update record from c# Linq to Ax with impersonation runAs as a Active Directory Windows User

when you update a record you might want to store who made the last change

image

when you are within ax this is done automatically as long as you have set on your table the ModifyBy property to yes.

if you use Linq to Ax using .Net interop to X++ it becomes a bit more complex.

on the example you find here https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp
they connect using this code

         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

which will cause that it will always mark the modifiedBy user to the AOT user.

there is a different way to open the session in C# which is axSession.LogonAs
so you can do:

U23.Session axSession = new U23.Session();
System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
var strUserName = nc.UserName;
axSession.LogonAs(strUserName.Trim(), "yourDomain.com", nc, "fch", "en-GB", null, null);

which works perfectly fine if you have the Active Directory username and password.

If you authenticate your client web site using forms authentication and the user enters his username and password you are fine, you have them in clear (just don’t tell it to a security expert…). Otherwise if you use for example windows authentication you need to start messing up with impersonation to pass the network credentials to you the API.

here an introduction: How To: Use Impersonation and Delegation in ASP.NET 2.0

it is quite a complex subject, so I decided to use a different approach and leverage the runAs method.

here the class I created:

//This class is useful when doing a record update from Linq to Ax. otherwsie it would take the AOS user as the user making the change.
class MYD_UpdateImpersonated
{
    Common common;
    UserId userId;
}
private void new()
{
}
public static MYD_UpdateImpersonated construct()
{
    return new MYD_UpdateImpersonated();
}
protected Common parmCommon(Common _common = common)
{
    common = _common;

    return common;
}
protected UserId parmUserId(UserId _userId = userId)
{
    userId = _userId;

    return userId;
}
public RecId update()
{
    container args;
    container result;
    container recordBuf;
    TableId tableId;
    RecId recId;

    RunAsPermission permission;

    recordBuf = buf2Con(common);
    args = [common.TableId, recordBuf, common.RecId];

    permission = new RunAsPermission(userId);
    permission.assert();

    // Need use the runas method; we want to run as the user to update records
    // BP deviation documented
    result = runAs(userId, classNum(MYD_UpdateImpersonated), staticMethodStr(MYD_UpdateImpersonated, updateAsUser), args);
    CodeAccessPermission::revertAssert();

    [recId] = result;
    return recId;
}
private static container updateAsUser(container _args)
{
    DictTable dictTable, dictTableBuf;
    Common common, commonBuf;
    TableId tableId;
    container recordBuf;
    RecId recId;

    [tableId, recordBuf, recId] = _args;

    dictTable = new DictTable(tableId);
    dictTableBuf = new DictTable(tableId);

    common = dictTable.makeRecord();
    commonBuf = dictTableBuf.makeRecord();

    con2Buf(recordBuf, commonBuf);

    ttsBegin;
    common.selectForUpdate(true);
    select common where common.RecId == recId;
    buf2Buf(commonBuf, common);
    common.update();
    ttsCommit;

    return [common.RecId];
}
public static server MYD_UpdateImpersonated newFromCommon(Common _common, UserId _userId)
{
    MYD_UpdateImpersonated updateImpersonated;

    updateImpersonated = MYD_UpdateImpersonated::construct();
    updateImpersonated.parmCommon(_common);
    updateImpersonated.parmUserId(_userId);

    return updateImpersonated;
}

now in c# you can create an extension class like this:

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.AX.Framework.Linq.Data;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class GenericRecord
    {
        public static long UpdateRecord<T>(this T axTable, string windowsUser) where T : Common
        {
            var userInfo = AifPortUser::getAxaptaUser(windowsUser);// in the format  DOMAIN\USERNAME
            //if (!userInfo.isActiveDirecroyUser()) return 0;//if you prefer rather that an error is thrown from MYD_UpdateImpersonated class if the user is invalid
            var updateImpersonated = MYD_UpdateImpersonated.newFromCommon(axTable, userInfo.getUserId());
            return updateImpersonated.update();
        }
}

now we create our repository class like this:

public void UpdateDiscussion(Discussion discussion, string windowsUser)
{
    //see: https://enricoariel.blogspot.com/2019/08/proxy-classes-for-net-interop-to-x.html
    //on how to open a session inside a using statement
    using (var axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        var workerRecId = HcmWorker.findByPersonnelNumber(discussion.Worker).RecId;
        var hcmDiscussion = HcmDiscussion.findByDiscussionWorker(discussion.DiscussionId, workerRecId);

        hcmDiscussion.status = (HcmDiscussionStatus) discussion.Status.Value;
        hcmDiscussion.UpdateRecord(windowsUser);  
    }
}

finally here my discussion dto Poco classes

namespace MydDynamicsIntegration.Models
{
    public class Discussion
    {
        public string DiscussionId { get; set; }
        public AxEnumDefinition Status { get; set; }
        public string Worker { get; set; }
    }
}

namespace MydDynamicsIntegration.Models
{
    public class AxEnumDefinition
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public string Label { get; set; }
    }
}

get Ax UserId from from NetworkAlias in Dynamics Ax

I started by wring something like this

public static UserInfo findUserInfo(NetworkAlias _networkAlias)
{
    UserInfo userInfo;

    select firstOnly userInfo
        where userInfo.networkAlias == _networkAlias;

    return userInfo;
}

this code does the job, but after some research I could find a built in method. I think it is always better to use native code then write your own. It handles many more variables that I didn’t even think of. if you look inside (as you always should) you will see that the above code is quite incomplete.

static void JobUSerInfoFromWindowsUser(Args _args)
{
    UserInfo userInfo;
    AifWindowsUser windowsUser = 'myDomanin.com\\ActiveDirecotryUserName';
    userInfo = AifPortUser::getAxaptaUser(windowsUser);
    info(userInfo.id);
}

Tuesday, August 27, 2019

display method caching in Dynamics Ax

I just got into a problem with a display method which let me dig a bit more on the subject. Let me share with you, maybe is nothing new, but worth remembering.

I created my method on the table like this:

[SysClientCacheDataMethodAttribute(true)]
public display MYD_AnnualContractValue contractValue()
{
    MYD_AnnualContractValue value;

    value = // code to get the value...

    return value;
}
first one consideration (from http://www.axaptapedia.com/CacheAddMethod)

Performance considerations

When caching is enabled for a display or edit method, that result of the method will be calculated on the AOS and passed through to the client with the result set. In general, this is faster than calling the method directly from the client, on demand, as it reduces client-server calls.

This makes good sense when a display method is used as part of a grid, but in other circumstances it is important to gauge whether the cost of having the method calculated every time is worth the reduction in client-server calls. An uncashed display method is triggered only when the control is visible, so it can be better to not use caching for display methods bound to controls on tab pages which are rarely viewed.

Attribute SysClientCacheDataMethodAttribute set to true if you want it updated when a record is saved

Look in \Classes\SysClientCacheDataMethodAttribute\new and see that when you set true the display cache is update on record save. When you set as false it is not. meaning that the display method on the form will not change on update until you close and open the form again

refresh the display when you modify a record (before you save it)

you have two options:

OPTION 1

the simplest option is just to remove the SysClientCacheDataMethodAttribute completely

//[SysClientCacheDataMethodAttribute(true)] //Enrico: do not use cache so the value changes immediately without need to save.
public display MYD_AnnualContractValue contractValue()
{
    //
}

which might make sense especially if this method is not expected to go inside a grid.

OPTION 2

use cacheCalculateMethod as explained in this post: https://smrithisomanna.blogspot.com/2008/12/using-cacheaddmethod.html
which works also if you set the cache on the table rather then using the cacheAddMethod on the datasource.

under the datasource on the field/s that is/are triggering the display method to be updated override the modify method like this:

image

public void modified()
{
    super();
    MyTable_ds.cacheCalculateMethod(tablemethodstr(MyTable, contractValue));
}
in this way you can leave the caching attribute on the table display method so that it is always cached by default and force a refresh programmatically.

Proxy Classes for .NET Interop to X++: session open inside using statement

When you start using Proxy Classes for .NET Interop to X++ you need to open and close a session and in order to adhere to the DRY principle (do not repeat yourself) I created a new c# class to use in my project.

if you look in the examples of how to use Linq to Ax (https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp) you can see in the code they have to open and close the session at the end:

using       System;  // C#
using       System.Linq;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;
using       Microsoft.Dynamics.AX.Framework.Linq.Data; // .ForUpdate() needs this.

namespace LinqProviderSample
{
   class Program  // C#, LINQ to AX.
   {
      static void Main(string[] args)
      {
         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

    //more code...

    axSession.Logoff();
      }
   }
}

but what I want is to use a using statement like this so that I do not have to worry about opening and closing the session and repeat every time the same code:

public string GetTableLabel()
{
    using (AxSessionManager axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        MyTable record = new MyTable();

        var x = new SysDictTable(record.TableId);

        return x.label();
    }
}

Here the class you need to add to the project:

using System;
using System.Net;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public class AxSessionManager : IDisposable
    {
        private readonly U23.Session _axSession = new U23.Session();

        public U23.Session Connection
        {
            get { return _axSession; }
        }

        /// <summary>
        ///     Checks to see if the AX session is connected. If it's null we return false.
        ///     Then we check to see if it's logged in, then return true. Otherwise it's not logged in.
        /// </summary>
        public bool Connected
        {
            get { return _axSession != null && _axSession.isLoggedOn(); }
        }

        public void Dispose()
        {
            CloseConnection();
        }

        /// <summary>
        ///     This connects to the AX session. If it's already connected then we don't need to connect
        ///     again, so we return true. Otherwise we'll try to initiate the session.
        /// </summary>
        /// <returns>
        ///     True: Connection openned successfully, or was already open.
        ///     False: Connection failed.
        /// </returns>
        public bool OpenConnection()
        {
            if (Connected)
            {
                return true;
            }

            try
            {
                _axSession.Logon("fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool OpenConnectionAs(NetworkCredential nc, string domain)
        {
            //System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
            var strUserName = nc.UserName;

            if (Connected)
            {
                return true;
            }

            try
            {
                //_axSession.LogonAs(strUserName.Trim(), "domain.com", nc, "fch", "en-GB", null, null);
                _axSession.LogonAs(strUserName.Trim(), domain, nc, "fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        ///     If the session is logged on we will try to close it.
        /// </summary>
        /// <returns>
        ///     True: Connection closed successfully
        ///     False: Problem closing the connection
        /// </returns>
        public bool CloseConnection()
        {
            bool retVal;
            if (Connection.isLoggedOn())
            {
                try
                {
                    _axSession.Logoff();
                    retVal = true;
                }
                catch
                {
                    retVal = false;
                }
            }
            else
            {
                retVal = true;
            }

            Connection.Dispose();
            return retVal;
        }
    }
}


as you see from the code you can either open the connection using
axSession.OpenConnection();
or use OpenConnectionAs where you need to pass the network credentials of the logged in user that match the user in Dynamics to impersonate that user in the session.

validate UK postcode using a free WebService in x++ Dynamics Ax 2012

in Uk there is this free service http://postcodes.io/ which can be used to validate a uk postcode.

here an example on how to use it in x++ using a Web Service and parse the returned Json

static void JobEF_WebServiceTestInDialog(Args _args)
{
    Dialog dialog;
    DialogField zipCodeField;
    LogisticsAddressZipCodeId postcode;
    str result = '"result":';
    str postcodeServiceUri = 'http://api.postcodes.io/postcodes/%1/validate';
    str values, ret;
    int ptr, len;
    System.Net.WebClient webclient;
    str uri;
    NoYesId validPostCode;
    
    webclient = new System.Net.WebClient();
    
    dialog = new Dialog('Validate UK postcode using Web Service');
    
    zipCodeField = dialog.addField(ExtendedTypeStr(LogisticsAddressZipCodeId));
    
    if (dialog.run())
    {
        postcode = zipCodeField.value();
        if (!postcode)
        {
            warning('please select a post code');
            return;
        }
        
        uri = strfmt(postcodeServiceUri, postcode);
        
        // Get the value from the server given the URL.
        values = webclient.DownloadString(uri);

        // Find the result value from the JSON
        len = strLen(values);
        ptr = strScan(values, result, 1, len);
        ret = subStr(values, ptr + strLen(result), len - ptr - strLen(result));
        validPostCode = ret == 'true';
        
        info(strFmt('"%1" is %2', postcode, validPostCode ? 'a valid postcode' : 'an invalid postcode'));
    }
}

Serialize to json using x++ in Dynamics Ax 2012

here an example how to create from x++ a simple json object.

static void JobEF_ListToJson(Args _args)
{
    int i;
    str myJson;
    System.Web.Script.Serialization.JavaScriptSerializer ser = new System.Web.Script.Serialization.JavaScriptSerializer();
    System.Object oVar;
    System.Type listType = System.Type::GetType('System.Collections.ArrayList');
    CLRObject arrayList = System.Activator::CreateInstance(listType);

    new InteropPermission(InteropKind::ClrInterop).assert();
    for( i = 0; i <= 5 ; i++)
    {
        oVar = strFmt('%1 is the value %2', i, DateTimeUtil::utcNow());
        arrayList.Add(oVar);
    }

    myJson = ser.Serialize(arrayList);

    info(myJson);
}

Create a unique random number in Dynamics Ax x++

static void JobEF_RandomNumber(Args _args)
{
    System.Random rand = new System.Random();
    MYTable MYTable;
    int value = rand.Next();
    boolean found;

    info(strFmt('%1', value));
    
    while (!found)
    {
        value = rand.Next(10);
        if ((select firstOnly MYTable where MYTable.ChangeRequestId == value).RecId == 0)
            found = true;
    }
    

    info(strFmt('%1', value));
}

helper class to clear the value of a table field

static boolean clearValueOfField(
    Common _record,
    FieldID _fieldID)
{
    DictField dictField;
    ;

    dictField = new DictField(_record.TableId, _fieldID);
    if (!dictField)
        return false;

    switch (dictField.baseType())
    {
        case Types::String :
        case Types::VarString :
            _record.(_fieldID) = "";
            return true;

        case Types::Date :
            _record.(_fieldID) = datenull();
            return true;

        case Types::Real :
        case Types::Integer :
        case Types::Enum :
            _record.(_fieldID) = 0;
            return true;

        default :
            return false;
    }

    return false;
}

Effective date Form datasource filter Queryrange using check boxes to select expired active future records in Dynamics Ax

I had a requirement to create create a form where the date effective selection appears as check boxes.

image

I found a similar implementation in LogisticsPostalAddress, so here the code I will show you below based is on what you can see in \Forms\LogisticsPostalAddress\Data Sources\LogisticsLocation\Methods\executeQuery

1) override the click event of each checkbox like this:

public void clicked()
{
    super();

    element.setEffectiveDateFilter();
}

2) add this method

here I added some logic to check uncheck based the checkboxes to avoid invalid selections

public void setEffectiveDateFilter()
{
    if (DisplayExpired.value() && !DisplayActive.value() && DisplayFuture.value())
        DisplayActive.value(true);
    if (!DisplayExpired.value() && !DisplayActive.value() && !DisplayFuture.value())
        DisplayActive.value(true);

    MYTable_ds.executeQuery();
}

3) override the executeQuery of the datasource

public void executeQuery()
{
    //based on the template: \Forms\LogisticsPostalAddress\Data Sources\LogisticsLocation\Methods\executeQuery
    QueryBuildRange qbrValidFrom, qbrValidTo;
    RecId curRecordRecId;
    ValidFromDate validFrom = Global::dateNull();
    ValidToDate validTo = Global::dateMax();
    boolean expired;
    boolean active;
    boolean future;
    str queryRangeStr='';

    // Get the record currently selected
    curRecordRecId = MYTable_DS.cursor().RecId;

    expired = DisplayExpired.value();
    active = DisplayActive.value();
    future = DisplayFuture.value();

    qbrValidFrom = SysQuery::findOrCreateRange(this.query().dataSourceNo(1), fieldNum(MYTable,ValidFrom));
    qbrValidTo   = SysQuery::findOrCreateRange(this.query().dataSourceNo(1), fieldNum(MYTable,ValidTo));

    if (expired && !active && !future)
    {
        queryRangeStr = '(%1.%3 <= %4)';
        MYTable_ds.query().validTimeStateDateRange(Global::dateNull(), systemDateGet());
    }
    else if (!expired && active && !future)
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(true,false,false);
        MYTable_ds.query().resetValidTimeStateQueryType();
    }
    else if (!expired && !active && future)
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(false,false,true);
        MYTable_ds.query().validTimeStateDateRange(systemDateGet(), dateMax());
    }
    else
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(true,true,true);
        validFrom = element.calcValidFrom();
        validTo = element.calcValidTo();
        //Debug::printDebug(strFmt('validFrom %1; validTo: %2', validFrom, validTo));
        MYTable_ds.query().validTimeStateDateRange(validFrom, validTo);
    }

    MYTable_ds.validTimeStateUpdate(ValidTimeStateUpdate::Correction);

    if (queryRangeStr)
        {
            qbrValidFrom.value(
                strFmt(queryRangeStr,
                    this.query().dataSourceTable(tableNum(MYTable)).name(),
                    fieldStr(MYTable,ValidFrom),
                    fieldStr(MYTable,ValidTo),
                    DateTimeUtil::toStr(DirUtility::getCurrentDateTime())
                )
            );
        }


    super();

        //focus again on the previously selected value
    MYTable_DS.findValue(fieldname2id(MYTable.TableId, 'RecID'), int642str(curRecordRecId));
}

Tuesday, August 20, 2019

get a list of Dynamics Ax security roles in code x++

static void FCH_SecurityRole(Args _args)
{
    SecurityRole        role;
    SecurityUserRole    userRole;

    while select userRole
    {
        info(userRole.User);

        select firstOnly role where role.RecId == userRole.SecurityRole;

        info(strFmt('%1 - %2', userRole.SecurityRole, SysLabel::labelId2String(role.Name)));

        info('--------------');
    }
}

dynamically create a Query inquiry in x++ code on Dynamics ax

if you try to run this job

static void JobEF_QueryInquiry(Args _args)

{

    QueryBuildDataSource queryBuildDataSource;

    QueryBuildRange queryBuildRange;

    QueryRun queryRun;

    Query    query;

    Qty total;

    InventTrans inventTrans;

    inventDim inventDim;

    query = new Query();

 

    queryBuildDataSource = query.addDataSource(tableNum(InventTrans));

    queryBuildDataSource.addSelectionField(fieldNum(InventTrans,Qty),SelectionField::Sum);

    queryBuildRange      = queryBuildDataSource.addRange(fieldNum(InventTrans,ItemId));

    queryBuildDataSource = queryBuildDataSource.addDataSource(tableNum(InventDim));

    queryBuildDataSource.addGroupByField(fieldNum(InventDim,InventBatchId));

    queryBuildDataSource.relations(true);

    queryRun = new QueryRun(query);

 

    if (queryRun.prompt())

    {

        while (queryRun.next())

        {

            inventTrans = queryRun.get(tableNum(InventTrans));

            inventDim   = queryRun.get(tableNum(inventDim));

            info(strFmt("Batch %1, qty %2",inventDim.inventBatchId, inventTrans.qty));

        }

    }

}


it will prompt you with a query inquiry form that you can fill up with the appropriate values to set the filters


image

container to numbered list

an example how to convert a container to a numbered list that we will show into a dynamically built form:

class MYD_Functions

{

}

public static Notes conToBulletText(container _con)

{

    TextBuffer txtb = new TextBuffer();

    int i;

 

    if (!_con)

        return '';

 

    try

    {

        for( i = 1; i <= conLen(_con) ; i++)

        {

            if (conPeek(_con, i))

                txtb.appendText(strFmt('%1%2) %3', i==1 ? '' : '\n', i, conPeek(_con, i)));

        }

 

    }

    catch (Exception::Error)

    {

        txtb.appendText('Error DFFMLIKQXM: could not translate the container to Notes');

    }

    return txtb.getText();

}

public static void conToBulletTextShowInForm(container _con)

{

    Args args;

    Form form;

    FormRun formRun;

    FormBuildDesign formBuildDesign;

    FormBuildStringControl formBuildStringControl;

 

    Notes notes = MYD_Functions::conToBulletText(_con);

 

    //I need to show it in a form text, in an info box would not show the new lines

    form = new Form();

 

    formBuildDesign = form.addDesign("Design");

    form.design().height(300);

    form.design().caption('Notes');

 

    formBuildStringControl =

        formBuildDesign.addControl(FormControlType::StaticText, "txt");

    formBuildStringControl.text(notes);

    formBuildStringControl.height(200);

 

    args = new Args();

    args.object(form);

 

    // Create the run-time form.

    formRun = new FormRun(Args);

    formRun.run();

    formRun.detach();

}

 

To try we can use a job:

static void MYD_Functions_conToNotes1(Args _args)

{

    container con = ['test1', 'test2'];

    container con1 = ['test3', 'test4'];

   

    MYD_Functions::conToBulletTextShowInForm(Global::conUnion(con, con1));

}



clip_image001

use c# Regular Expressions match to validate an email or a phone number

I share here a helper method to use c# Regular Expressions.

class MYD_Functions
{
}
public static boolean matchRegularExpression(str pattern, str value)
{
    System.Text.RegularExpressions.Match myMatch;

    myMatch = System.Text.RegularExpressions.Regex::Match(value, pattern);

    return myMatch.get_Success();
}
public static boolean validateEmail(EmailBase email)
{
    Str emailPattern = @'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$';

    return MYD_Functions::matchRegularExpression(emailPattern, email);
}
public static boolean validatePhone(Phone phone)
{
    Str phonePattern = @'^([0-9-+ ]{4,20}|)$'; //only num +- space; empty or min lenght of 4 to 20 char
    return MYD_Functions::matchRegularExpression(phonePattern, phone);
}

but I suggest you always use built in methods rather then creating yours. So here the same example using the Global::isMatch method (which behind the scene is actually doing the same thing

public static boolean validateEmail(EmailBase email)
{
    Str emailPattern = @'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$';

    return Global::isMatch(email, emailPattern);
}
public static boolean validatePhone(Phone phone)
{
    Str phonePattern = @'^([0-9-+ ]{4,20}|)$'; //only num +- space; empty or min lenght of 4 to 20 char
    return Global::isMatch(phone, phonePattern);
}

get the postal address in x++ Dynamics Ax

in order to get the Logistics postal address in Dynamics Ax you have different options. Here some examples.

For this example I choose to search for the postal address of an operating unit (department), but the same concept is valid for any other entity (clients, suppliers, workers…)

static void JobEF_AddressDepartment(Args _args)

{

    OMOperatingUnit department =  OMOperatingUnit::find(

                    MYD_HcmPositionDepartmentUtil::getOMDepartmentRecIdFromUnitNumber('123'),

                    OMOperatingUnitType::OMDepartment);

    DirParty dirParty = DirParty::constructFromCommon(department);

    RecId dirPartyRecId = dirParty.getPartyRecId();

 

    LogisticsLocationEntity logisticsLocationEntity = dirParty.getPrimaryPostalAddressLocation();

    LogisticsLocationRecId logisticsLocationRecId = logisticsLocationEntity.parmLocationRecId();

 

    //I have different options

 

    //1) I can instatiate the view DirPartyPostalAddressView

    DirPartyPostalAddressView dirPartyPostalAddressView = dirParty.getPostalAddress(logisticsLocationRecId);

 

    //2) I can instantiate LogisticsPostalAddress

    LogisticsPostalAddress logisticsPostalAddress = LogisticsPostalAddress::findByLocation(logisticsLocationRecId);

 

    //3) I can instantiate LogisticsPostalAddress in just one method:

    LogisticsPostalAddress logisticsPostalAddress1 = DirParty::primaryPostalAddress(dirPartyRecId);

 

    info(DirParty::getAddress(dirPartyRecId));

    info(dirPartyPostalAddressView.City);

    info(logisticsPostalAddress.City);

    info(logisticsPostalAddress1.City);

    info(dirPartyPostalAddressView.Street);

    info(logisticsPostalAddress.Street);

    info(logisticsPostalAddress1.Street);

 

    info(logisticsPostalAddress1.StreetNumber);

    info(logisticsPostalAddress1.Street);

    info(logisticsPostalAddress1.City);

    info(logisticsPostalAddress1.ZipCode);

    info(logisticsPostalAddress1.State);

    info(logisticsPostalAddress1.County);

    info(logisticsPostalAddress1.CountryRegionId);

    info(logisticsPostalAddress1.PostBox);

    info(logisticsPostalAddress1.DistrictName);

}

 

Get Active Directory user name details from x++ in Dynamics Ax

here an example how you can query your active directory and get user details like email, sid, name

static void JobEF_ActiveDirectoryUserDetails(Args _args)
{
    xAxaptaUserManager                  axUsrMgr;
    xAxaptaUserDetails                  axUsrDet;

    #define.NewDomain("yourDomain.com")

    axUsrMgr = new xAxaptaUserManager();

    try
    {
        axUsrDet = axUsrMgr.getDomainUser(#NewDomain, 'activeDirectoryUserName');

        if (axUsrDet)
        {
            info(strFmt('%1', axUsrDet.getUserMail(0)));
            info(strFmt('%1', axUsrDet.getUserSid(0)));
            info(strFmt('%1', axUsrDet.getUserName(0)));
        }

    }
    catch
    {
        info('Could not find');
    }
}

but please note AxaptaUserManager runs on the Client.
This cannot be used with the new batch framework. Since new batch
ramework runs on the server and we cannot call code on the client.

Convert a List to Array in Dynamics Ax

here a simple helper method to convert a List collection to an Array.

static Array arrayFromList(List _list)
{
     Array array;
     ListIterator listIterator;
     int i;

    array = new Array(_list.typeid());
     listIterator = new ListIterator(_list);

    for (i = 1; listIterator.more(); ++i)
     {
         array.value(i, listIterator.value());
         listIterator.next();
     }

    return array;
}

Chart of account get ordered list from x++ in Dynamics Ax

If you want to get the list here in the Chart of accounts in code is quite simple, here below an example.

image

static void FCH_chartOfAccountsList(Args _args)

{

    LedgerChartOfAccountContract chartOfAccounts = new LedgerChartOfAccountContract();

    DimensionServiceProvider serviceProvider = new DimensionServiceProvider();

    List mainAccountsList;

    MainAccountContract contract;

    ListEnumerator mainAccountsListEnumerator;

 

    chartOfAccounts.parmName("COABasic");

 

    mainAccountsList = serviceProvider.getMainAccountsForLedgerChartOfAccount(chartOfAccounts);

    mainAccountsListEnumerator = mainAccountsList.getEnumerator();

 

    while (mainAccountsListEnumerator.moveNext())

    {

        contract = mainAccountsListEnumerator.current();

        info(strFmt('%1 - %2 - %3 - %4',

                contract.parmMainAccountId(),

                contract.parmName(),

                contract.parmType(),

                MainAccountCategory::findAccountCategoryRef(contract.parmAccountCategoryRef()).AccountCategory));

    }

}


image

There is just a little issue you might want to address. As you noticed the list is not in the ordered.

If you need an ordered list, since you can’t order the collection List, we can convert it to a Map, which by default orders by its key value.

static void FCH_chartOfAccountsListOrderd(Args _args)

{

    LedgerChartOfAccountContract chartOfAccounts = new LedgerChartOfAccountContract();

    DimensionServiceProvider serviceProvider = new DimensionServiceProvider();

    List mainAccountsList;

    MainAccountContract contract;

    ListEnumerator mainAccountsListEnumerator;

    Map mainAccountsMap = new Map(Types::String, Types::Class);

    MapEnumerator mainAccountsMapEnumerator;

 

    chartOfAccounts.parmName("COABasic");

 

    mainAccountsList = serviceProvider.getMainAccountsForLedgerChartOfAccount(chartOfAccounts);

    mainAccountsListEnumerator = mainAccountsList.getEnumerator();

 

    while (mainAccountsListEnumerator.moveNext())

    {

        mainAccountsMap.insert(

                    mainAccountsListEnumerator.current().parmMainAccountId(),

                    mainAccountsListEnumerator.current());

    }

   

    mainAccountsMapEnumerator = mainAccountsMap.getEnumerator();

   

    while (mainAccountsMapEnumerator.moveNext())

    {

        contract = mainAccountsMapEnumerator.currentValue();

       

        info(strFmt('%1 - %2 - %3 - %4',

        contract.parmMainAccountId(),

        contract.parmName(),

        contract.parmType(),

        MainAccountCategory::findAccountCategoryRef(contract.parmAccountCategoryRef()).AccountCategory));

 

    }

}

 

now it orders by account number.

image