Quantcast
Channel: AX Code | Stoneridge Software
Viewing all 71 articles
Browse latest View live

Differences in Form Datasource Methods for Refreshing Data in Dynamics AX

$
0
0

Recently, I had been working on some rather complex customizations to a form in Dynamics AX, and found myself getting bogged down in refreshing the data on the form. There are many different ways to refresh the data, but each can get the information from different places (form cache, database, etc.). For what I was doing, I did not want to use the element.task() functionality. More information on this can be found here on the Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/hh812104.aspx).

I ended up using the old tried and true datasource_ds methods to control the functionality I needed. I found that I needed a bit of a refresher to remember exactly where each call goes to get the data.  The following article and tutorial is extremely helpful in explaining common usage of the four datasource methods: Tutorial: refresh, reread, research, executeQuery – which one to use?

If you are not familiar with the different calls, this is a great article explaining how each of the calls works, and where it retrieves the information. Also, if you ARE familiar with the calls it never hurts to have a refresher so you use the right method at the right time. Our development team uses this information often and we’ve found its the best source explaining the exact differences.

Happy DAX’ing…

 


Helpful TFS Queries for the Dynamics AX Developer

$
0
0

TFS can be a wonderful tool for the Dynamics AX Developer to help track bugs, tasks, etc. If it is used correctly, it can greatly increase your productivity.

Disclaimer #1: I will not be writing on the Source Control functionality of TFS in this blog).

Disclaimer #2: I will not be writing on the Source Control functionality of TFS into AX. If you would like, you can find more information on this topic in this post: AX and TFS in Daily Development Activities

Queries

The most helpful thing you’ll need is queries. Lots and lots of queries. Queries will be your lifeline into TFS. In practice, I have a plethora of queries I create at the start of the project. Here is a list of a few of the ones you may want to consider as well:

“Tasks for Sprint X”:  I create a query for each Sprint “All DEV Tasks” “Active Bugs” “Resolved Bugs” “All Bugs” “My Sprint Work Items” “Change Request for DEV” (If your organization uses Features instead of Tasks, just change the wording above.)

If you haven’t created a query in TFS before it is pretty simple.

David B_Microsoft Visual Studio

 

Make sure you are in the Work Items area. From there simply click the New Query button and you will be presented with screen above. Now it is just a matter of filtering down your results.

Here are a couple of examples of queries:

All Bugs

David B_And 2

Active Bugs

David B_And 3

All FDDs

David B_And 6

This query is a bit more interesting as you can see that TFS allows us to group criteria together. To accomplish this, simply select all the rows you want to group, right-click, and select “Group Clauses”.

Once you have created your queries, simply click the “Save Query” button and put it wherever it belongs. You will notice in the Save dialog that there are two main folders you can save your queries. A “My Queries” folder and a “Shared Queries” folder. As it implies, the “My Queries” folder only you have access to. The “Shared Queries” folder is shared among all TFS users. This is a nice place to put a majority of these queries so that everyone can easily see them.

The other thing you may notice is that by default there is a “My Work Items” query in the “Shared Queries” folder. This query will show you everything that is currently assigned to you.

David B_And 3 Shared Steps

First, notice it uses the keyword @Me. This indicates the current logged in user. As such, you should be very wary of changing this query as it will affect everyone. To get around this, I create a “My Sprint Work Items” query. It essentially is a copy of the “My Work Items” query with a few other pieces added in.

David B_And 4

You can see that I’ve added the “Assigned To” to be myself (you could leave it as @Me as well though) and I’ve added a filter on the Iteration Path (aka current Sprint/workflow, etc). Feel free to personalize this one as much as necessary. The important part of this query is that it will not be saved in the “Shared Queries” category, but in the “My Queries” folder instead.

Now that you have created all your queries, all of the wonderful data housed in TFS is easily at your fingertips. Simply double click on a query and up comes your results set. From here you can sort, add columns, filter to your hearts content!

Hopefully, you can see the usefulness of all of this. As you go through the project you can easily find bugs and the given state of a bug, FDD/Tasks become easier to track, and finding your work is easier than ever.

 

Local Macros and Regular Expressions in Dynamics AX 2012

$
0
0

I was recently working on a customer project where I wanted to use regular expressions to do string comparison in Dynamics AX 2012. The customer wanted to identify the type of card and validate the card from the card number, based on the range of card numbers assigned to the card issuer. We can use regular expression to compare the card number with major providers and derive the type of card and validity of the card number.

Shortcomings in the #define Command

X++ provides several pre-compiler directives that we can use. The directives are processed before the resulting X++ code is given to the X++ compiler. These directives declare and handle macros and their values.

I decided to use the #define command, to store the constant value for regular expression for various card issuers in a macro. I ran into an existing shortcoming in the #define command. When we have closing brackets ‘)‘ in a macro value, Dynamics AX gives a compiler error with the description ‘Syntax error.’

Ramesh S_Static Void

 

Ramesh S_Compiler Output

 

The workaround for this is to use localMacro instead.

 Ramesh S_Jobs-Editor

 

 

How to use Regular Expression in Dynamics AX

A regular expression is a pattern that the regular expression engine attempts to match in input text. Dynamics AX also provides extensive support for regular expression. Using Regular Expression in Dynamics AX is very simple, due to the ability to call .NET objects from AX.

Ramesh S_Static Void Long

 

Happy coding!

 

Solutions to Blank Page Issues in Dynamics AX

$
0
0

Blank pages – It’s a problem that happens to many Dynamics AX users at some point. Some of our most frequently searched for and viewed blog posts are related to the topic of blank page issues in Dynamics AX.

Here is a compilation of blog posts we’ve put together over the years to address some of the root causes of blank pages and their solutions. Hopefully, you’ll find some useful tips for dealing with this pesky problem.

Blank Page Issues – SSRS Reports

This blog covers the what your blank page issue might be, why the issue is arising and several possible scenarios and solutions.

Blank Page Issues – SSRS Reports (Page Break) in AX 2012

The inspiration for this blog arose from working with check customizations and covers another problem and solution to blank pages, not covered in the previous blog.

Another Blank Page Problem in Microsoft Dynamics AX 2012

This blog covers yet an additional blank page problem more often mentioned in SSRS blogs unrelated to AX. It covers issues where a blank page will print, only when the detail lines take up the entire first page.

Dynamics AX Development: Check Basics

Covering the basics of check customization in Dynamics AX, this post points out some tips to avoid extra spaces and blank pages.

Developer Tips for Newbies: Part 1

In addition to blank page issues, here is another great blog that covers general tips for any developers new to Dynamics AX and a great reminders for those who aren’t.

 

How to Extend Sales Order Update Functionality to Custom Fields in Dynamics AX

$
0
0

I worked on a feature to add custom fields in Dynamics AX to the sales order header and lines. With this customization, there was a requirement that if you were to update the custom field on the sales order header, the value would also transfer to all the sales lines. Dynamics AX already does this for some fields out-of-the-box. In this article, I will show you how you can extend this attribute to your custom fields.

Once you have added the fields to the SalesTable and the SalesLine table and exposed the fields on the SalesTable form, add the new custom field to the HeaderToLineUpdate field group on the SalesTable Table.

Project Sales Table Update in Dynamics AX

In order for the system to recolonize the sales order to line fields that need to be updated, the fields must be set to prompt, or always, in the Account Receivable parameters form. (Accounts receivable – Setup – Accounts receivable parameters – Updates – Update order lines (Button))

To add this field to the update order lines form, you will need to modify the lineUpdateDescription method in the SalesTable2LineField class. Add the following code highlighted below, replacing the SSI_TransDate field with your new custom field.

Classes Sales Table in Dynamics AX

Once this is complete if you go back to the AR parameters form, you will now find your custom field listed on the Update order lines form. Note: make sure to mark the field as prompt.

Update order line in Dynamics AX

The sales order line update process uses the sales order document service, so you will need to add some custom code for the system to copy the value from the sales header to the sales lines.

In the AxSalesTable class I added a new method:

Code in Dynamics AX

In the AxSalesLine class I added the following new method:
Code in Dynamics AX

Code in Dynamics AX

The final piece to the puzzle is to add the set*YourCustomField* method to the setTableFields method in the AxSalesLine class:

Set Your Custom Field

Since you’re are modifying a service document, make sure to run an incremental CIL. But, once this is all complete you should find the sales order update functionality working for your custom fields.

Data Item Does not Exist When Calling AIF Document Service from C#

$
0
0

When calling an AIF document service from a C# you may run into the error “Data item %1 does not exist.”  This error can be particularly frustrating because you can see that you are specifying the value in your application.

You can view the XML received by the service by turning on document logging on the Inbound Port and viewing the AIF History.  This will verify whether the value was actually specified.

To enable document logging:

  1. Click System administration > Setup > Services and Application Integration Framework > Inbound ports
  2. Select your service port.  If it is active, you must deactivate it.
  3. Expand the Troubleshooting Fast Tab.
  4. Change the Logging mode value to Original document.
  5. Activate the port.

To view the XML for a particular service call:

  1. Click System administration > Periodic > Services and Application Integration Framework > History
  2. Select the entry you wish to view.
  3. Click the Document logs button.
  4. Click the View XML button.

The problem is that the value is not transmitted unless the corresponding PropertyNameSpecified property is set to true.  This property indicates that a property value, even if null, should be treated as the actual property value.

The solution is to make sure that you set table.{PropertyName}Specified = true for each value you specify.  If you are calling the service from C# you can create an event handler that makes this task easier.

private static AxdPurchPurchaseOrder getPurchaseOrder()
{
    AxdPurchPurchaseOrder order;
    AxdEntity_PurchTable purchTable;
    AxdEntity_PurchLine purchLine;

    purchTable = new AxdEntity_PurchTable();
    purchTable.PropertyChanged += PurchOrderService.purchTable_PropertyChanged;

    purchTable.AccountingDate = DateTime.Today;
    purchTable.CashDiscPercent = 0.00M;

}

static void purchTable_PropertyChanged(Object sender, System.ComponentModel.PropertyChangedEventArgs args)
{
    Type senderType = sender.GetType();
    string propertyName = String.Format("{0}Specified", args.PropertyName);
    PropertyInfo prop;

    prop = senderType.GetProperty(propertyName);

    if (prop != null)
    {
        prop.SetValue(sender, true);
    }
}

 

Locating Primary Contact Information for a Specific Warehouse using Dynamics AX

$
0
0

This week I am going to talk about how to find the primary contact person and their phone number for a specific warehouse. This information in Dynamics AX is defined in Warehouse management >> Setup >> Warehouse setup >> Warehouses. You locate the warehouse in question and go to the Address fast tab. I had to click the Edit button as the demo data does not have this defined for the address I was wanting. On the New address form, you can enter in the contact information in the Contact information fast tab.

Warehouse management

Once this is done, the following job will find the contact information for the warehouse tied to a specific shipment.

Warehouse management

static void testContactLogic(Args _args)
{
    WHSShipmentTable        		shipmentTable = WHSShipmentTable::find('USMF-000002');

    InventLocation                  	inventLocation;
    LogisticsEntityPostalAddressView    	postalAddressView;
    LogisticsElectronicAddress          	electronicAddress;
    LogisticsLocation                   	contactLocation;

    inventLocation = inventLocation::find(ShipmentTable.InventLocationId);
    if (inventLocation)
    {
        select firstonly postalAddressView
            where   postalAddressView.Entity     == inventLocation.RecId
            &&      postalAddressView.EntityType == LogisticsLocationEntityType::Warehouse
            &&      postalAddressView.isPrimary  == NoYes::Yes;

        if (postalAddressView)
        {
            select firstOnly electronicAddress where electronicAddress.Type == LogisticsElectronicAddressMethodType::Phone
            join contactLocation where contactLocation.ParentLocation  == postalAddressView.Location
                && contactLocation.RecId == electronicAddress.Location;

            info(strFmt("Primary contact name: %1  Primary contact phone: %2",electronicAddress.Description, electronicAddress.Locator));
        }
    }
}

Please note this code could be optimized with more joins. I intentionally created the job above in this manner to more clearly demonstrate the relations needed, AND make it easier to step through via the debugger if so desired.

Oh, of course, I should show the output of this job as well:

Primary contact information for warehouse management

The above code could easily the modified for email, all contacts, etc.  This just demonstrates retrieving specific desired information for a specific warehouse based on shipment information.

Happy coding.

Creating Cross Company Select Statements in X++

$
0
0

When creating a query in X++ by default that query will only return the values in the company that you are logged into.  If you desire to pull from multiple companies, you will need to use the crosscompany keyword.

CustTable custTable;
while select crossCompany custTable
		{
print custTable.accountNum;
		}
		pause;

The above code section will print the account numbers for each row in the CustTable and will include data from all companies.

If you would like to restrict the companies that you want data from you can also define a container that will define which companies to search.

container desiredCompanies = ['Company1', 'Company2'];
		CustTable custTable;
		while select crossCompany : desiredCompanies custTable
		{
			print custTable.accountNum;
		}
		pause;

The above code section will print the account numbers for each row in the CustTable for the two companies that we specified in the container.


TFS Code Repository and Dynamics AX

$
0
0

If you are using TFS as your Dynamics AX code repository, there are a few things you will want to do to make life easier for those around you.

Dynamics AX & TFS Integration

First, here is where you would go to setup your TFS integration in Dynamics AX 2012:

David B_Version Control

This will open this form:

David B_Form

Make sure to open both pages as well:

David B_Well

This page is actually more important than the first. You can see that here is where we will be setting up the connections to Dynamics AX. Work with your TFS admin to get the values for these.

Adding Code & Checking out Code

In order to add Dynamics AX code to your TFS repository, you must first make a change to bring the code into your current layer. So make your needed changes, then you can right-click on the object and choose “Add to Version Control.”

David B_AOT

To check out code you can either right-click on the object in the AOT and choose “Check out.”

David B_Check out

Or, there is a button on the toolbar if you are in the editor.

David B_Editor

Labels

A bit separate from this is labels.

David B_Labels

If you go to the label editor you will be presented with a few buttons across the top. When you need to create a new label, I recommend you do “Get latest” first. In theory, the system should do that during checkout, but I feel it is better to be safe and do it yourself.

David B_Yourself

The other thing to mention with labels is that if you have single checkout enabled (only one person can check out an item at a time) you need to comment out the following code.

($\Classes\SysVersionControlSystemFileBased\checkInChangelist)

David B_Code

Checking in Code

Once you are ready to check in, go to Version Control > Pending Objects. This will show you a list of everything that is ready to be checked in. You will notice that there is a “Check In” button here as well.

David B_Check in

You can also check in by right-clicking an object in your AOT and clicking “Check In”:

David B_Check in 2

Or, with the button on the editor:

David B_Editor 2

Once you click on the button a new dialog will show up.

David B_Show Up

Here, you can select what objects you want to check into TFS. You will also see a text box on the top of the tab page. Here is where you would put the description of your check in. I highly recommend a standard format to be used by all of the developers on your team. The format I use is:

<Initials> – FDD <###> (or) Bug <###> – <Description>

You should notice that there is a second tab page as well. Here you will choose what task to associate this check-in with. In general, it is best to associate a check-in with a TFS task. The list that you see is anything that you are assigned to in TFS. If you need to check in against a task that is not assigned to you, there is an “Add task” section on the top that will allow you to put in a TFS ID so you can check in against it.

David B_Checkin against it

Once your code is checked in, you can now go into Visual Studio and see the object.

David B_Object

From here you can view the history of the object and many other cool features.

Synchronizing Your Code

The other thing you want to make sure you do frequently is Synchronize your code in AX with what is in the TFS code repository. To do this simply click on Version Control > Synchronize.

David B_Synchronize

This will present you with a new window.

David B_Window

Simply click the “OK” button and it will pull in the latest version of the code into AX. You will notice that there is also a “Force” check box. Checking that check box tells AX to pull in EVERYTHING from source control. In general, you don’t need to do “Force” very often. The non-Force “incremental” synchronize is usually sufficient. My practice is to run this once a week (usually Monday) or if I know a large feature has been checked in that I want to pull down locally. How often you synchronize is really up to you.

If you do not want to pull everything down, you can sync one item at a time by going to the object in the AOT, right-clicking on it, and selecting “Get Latest.”

Davic B_Get Latest

Synchronization Log

The last thing to bring up is the “Synchronization log.” You can find it right under the Synchronize menu option. Version Control > Synchronization log. For the most part, you don’t need to worry about this form, but you may get an error when trying to sync down an object saying something to the effect of “This object was not imported properly, please check the synchronization log and try again.” The easiest way to fix this is to open the log and click the “Clean all” button. This will clear out the log and allow you to attempt to pull the object down again.

Synchronization log in TFS code repository

 

For more information on TFS queries for the Dynamics AX Developer check out my previous blog.

Happy coding!

Creating a Default Lookup Form with Filter in Dynamics AX

$
0
0

When working in Microsoft Dynamics AX have you ever found yourself reusing or copy/pasting the same code to create the same lookup over and over to custom forms and/or report parameters? Have you ever been asked if you could modify the lookup form to allow for filtering?  If the answer is yes, then you may be interested in creating a default lookup form with filter in Dynamics AX.

The standard lookup forms are usually nothing more than a grid. However, you can get as creative as you want with them to allow for fact showing when a record is selected or whatever else you can imagine that you would want to see on your lookup form. (A great example of a very functional and informative standard AX lookup form is the HCMWorkerLookup form.)

The following is a simple example of how to create a lookup form with the option to filter with prepopulated combo box values.

Create Extended Data Type

  • Create a new EDT type of string.
  •  Name: DinosaurName
  •  StringSize: 20

Project Default Lookup

Create Enum

  • Create a new base enum
  • Name: DinosaurMealPlan
  • Add 3 new elements
  1. Name: All
  2. Name: Herbivore
  3. Name: Carnivore

Project Default Lookup Examples

Create table and fields

  • Create new table
  • Name: Dinosaurs
  • Add 2 new fields
  1. Create Field type of type string
  • Name: DinosaurName
  • EDT : DinosaurName

2. Create field of type enum

  • Name: DinosaurName
  • EnumType : DinosaurName

Project Default Lookup Examples

Add data to table

We need some data to play with so insert the following values into the newly created Dinosaurs table.

Table

Create a Form

The first thing you need to do is to create a basic form.

Project Default Lookup Designs

Set form design properties

Now we need to set the design properties on the form so it behaves like a standard lookup. The following design properties need to be change.

  1. Frame: Border
  2. AlwayOnTop: Yes
  3. HideToolbar: Yes
  4. Style: Lookup

Add form groups

We want our grid to be separated from the drop down we will use the fileter the data. So we need to add two groups.

  1. Group will contain a grid
  • Name: MainArea

2. Group will contain our combo box

  • Name: Filter
  • Top: Top edge (this will allow our combo box to be displayed on the top left of the form)

Project Default Lookup Design

Add a Grid to design

  • Add grid to our group “MainArea”
    • DataSouce: Dinosaurs

Project Default Lookup Examples

Add a combo box to design

  • Add combo box to our group “Filter”
    • Name: cmbDiet
    • AutoDeclaration: Yes
    • EnumType: DinosaurMealPlan

Project Default Lookup Group Filter

Add form Data Source(s)

Drag the Dinosaurs table we created earlier to the data sources object on the form.

Project Default Lookup Example

Set form Data Source(s) Properties

The data source(s) properties need to be update as listed below.

  1. AllowEdit: No
  2. AllowCreate: No
  3. AllowDelete: No
  4. InsertAtEnd: No
  5. InsertIfEmpty: No

Add data source field to form grid

Now that we have our data source added let add one of its fields to our grid.

  1. Drag the filed DinosaurName to the grid on the form
  2. Update properties on newly add grid control
  • AutoDeclaration: Yes

Project Default Lookup Data Dictionary

Modify Form Methods

Now we need to modify the forms methods to give the functionality of a lookup form.

Init method

Now we need to override the forms init() to tell the form which control will be used as the selected value.

public void init()
{
    super();

    // Returns selected value from form control
    element.selectMode(Dinosaurs_DinosaurName);
}

Run method

We want to override the forms run() to allow filter to behave properly in the AX environment

public void run()
{
    // Allow filtering to work properly in a custom lookup form
    boolean filterLookup;
    FormStringControl callingControl = SysTableLookup::getCallerStringControl(element.args());

    filterLookup = SysTableLookup::FilterLookupPreRun(callingControl, Dinosaurs_DinosaurName, Dinosaurs_ds);

    super();

    SysTableLookup::FilterLookupPostRun(filterLookup, callingControl.text(), Dinosaurs_DinosaurName, Dinosaurs_ds);
}

 

Create form method

We will create a new method called updateQuery() to handle changes to our data sources query when the filter criteria changes in our combo box.

/// <summary>
/// Updates query based on forms combobox selection
/// </summary>
/// <returns>
///  Updated query
/// </returns>
/// <remarks>
///  2016-07-25 Lookup form example
/// </remarks>
public Query updateQuery()
{
    Query query;
    QueryBuildRange qbrDinosaurDiet;
    QueryBuildDataSource    qbdsDinosaur;

    // Get current query of data source
    query = Dinosaurs_ds.query();

    // Get the data source that we need to adjust the range
    qbdsDinosaur  = query.dataSourceName('Dinosaurs');

    // Checks if there is an existing range
    qbrDinosaurDiet  = qbdsDinosaur.findRange(fieldNum(Dinosaurs, DinosaurDiet));

    // If range does not exist we need to add a range
    if(qbrDinosaurDiet == null)
    {
        qbrDinosaurDiet  = qbdsDinosaur.addRange(fieldNum(Dinosaurs, DinosaurDiet));
    }

    // Selection of combo box will determine values for ranges
    switch(cmbDiet.selection())
    {
        case DinosaurMealPlan::Carnivore:
            qbrDinosaurDiet.value(SysQuery::value(DinosaurMealPlan::Carnivore));
            break;

        case DinosaurMealPlan::Herbivore:
            qbrDinosaurDiet.value(SysQuery::value(DinosaurMealPlan::Herbivore));
            break;

        default:
            qbrDinosaurDiet.value(SysQuery::valueUnlimited());
            break;
    }

    return query;
}

Modify data source method

We will override the Dinosaurs data source method executeQuery() to allow for our updates to the querys data sources ranges.

This will utilize the updateQuery() that we created in the form.

public void executeQuery()
{
    // update the datasources query
    this.query(element.updateQuery());

    super();
}

 

Modify Form Control methods

We need to override the method selectionChange() on the combo box with the following code.

Whenever a user changes their selection in the combo box it will update our query and thus change the look form to reflect their selection.

public int selectionChange()
{
    int ret;

    ret = super();

    // Executes data sources query which will filter our combo box
    Dinosaurs_ds.executeQuery();

    return ret;
}

Add as default lookup from for Extended Data Type ( EDT )

Ok, so now we are at the whole reason I started this article. I initially was not aware that you could assign a created lookup form to an EDT. Wow? Who cares, right? We should care because this allows us to better follow the DRY (Don’t repeat yourself) principle. Which of course helps with maintainability and reusability.

This comes in really handy when you are doing data contract classes for reports. Especially if you are doing a lot of custom reports that are using the same EDT as a parameter.  This allows you to reuse the lookup form without much extra code if any. Also if you don’t require any further complexity to the report parameters, then it also saves you from having to create a UIBuilder class to allow lookups for the parameters.

To set the lookup form as your lookup for the EDT set the following property on the selected EDT.

  • FormHelp: DefaultLookup

Call form from a lookup event

You can also call this from if you are overriding a lookup method on a form.

Below is an example of how you can accomplish this.

public void lookup()
{
    // Overrides a lookup method to utilize the default lookup form
    FormRun lookup;
    Args args = new Args(formStr(DefaultLookup));

    args.parm("");
    args.caller(this);
    lookup = classFactory.formRunClass(args);
    lookup.init();
    this.performFormLookup(lookup);
}

 

Now go implement the lookup form that was created and it should look like the images below.

Filter choices

Filter Choices

Carnivore filter selected

Filter selected

And there you have your lookup form with filter in Dynamics AX.

Changing a Relationship Precedence in Dynamics AX

$
0
0

Have you ever been in a scenario where a data source has more than one relationship to a table it is being linked to? I have. It wasn’t a problem until I wanted the second relationship to take priority.

So the real question is, how to specify what relationship should the table use?

relationship-precedence_brandon

 

In my example, on the InventTable I have multiple relationships defined. Please note though:

InventTable.AlcholoManufacturerID_RU == VendTable.AccountNum InventTable.PrimaryVendorId == VendTable.AccountNum

On my form: EcoResProductDetailsExtended, I added VendTable as a data source.

Well, how does AX know which relationship I want take precedence? By default, AX uses the first one. In this case, the AlcoholManufacturer_RU relationship takes precedence.

relationship-precedence_brandon-2

 

To change what relationship takes precedence I overloaded the init() method.

relationship-precendence_brandon-3

 

  1. First, you must tell the data source what relationship you want to use.
vendtable_ds.joinRelation("PrimaryVendTable");
2. Second, you must make the relationship the active relationship.
VendTable_ds.linkActive();
3. Third, you must execute the query.
vendtable_ds.executeQuery();
 

It is important to note, this must all be done before the super(). By following these simple steps, you will set a tables relationship precedence on a data source with more than one defined relationship.

Your init() method should look like the following.

public void init()
{
    vendtable_ds.joinRelation("PrimaryVendTable");
    VendTable_ds.linkActive();
    vendtable_ds.executeQuery();
    super();
}

 

Version Control in Dynamics AX: Check-in a project and all of its pending objects

$
0
0

There have been times in Dynamics AX, when I have had to check in a project that includes a lot of objects. Whenever we do a new design or bug fix we always create a project and then add in all the objects that we are making changes to. When completed, we check in the project and all objects with pending changes so they are all included in the same change set.

Sometimes you have a project that is very large and you also could have a lot of other pending objects. This can make the check-in a pain. You have to go through the whole list of pending objects and select the ones that you need. There is the potential of missing objects or selecting an object that isn’t part of the project and shouldn’t be part of check-in. To help resolve or make this easier I made a modification to version control.

Steps and examples to make this small change to the SysVersionControlCheckIn form are below:

1. Go to a project you want to check in. In this example, I have a project that is checked out and some objects that are also checked out.

version-control_eric

2. Right-click on that project and select Check In
3. On the Check-in form all objects in the project that are checked out will also be checked to be included in check in.

version-control_eric2

This is an easy and quick way to check in a project and all the objects included in that project. Instead of having to manually select each one you want to include in the check in. This will also eliminate the possibility of selecting an object that shouldn’t be part of the check in.

To include this functionality there is a small addition to the form SysVersionControlCheckedIn.

In method setSelectedAotElements I added the call to the new method.

void setSelectedAotElements()
{
    LastAotSelection lastAotSelection = new LastAotSelection();
    TreeNode treeNode;

    treeNode = lastAotSelection.first();

    element.setSelected(treeNode == null);

    while (treeNode)
    {
        select forupdate item
            where item.ItemPath == treeNode.treeNodePath();

        if (item)
        {
            item.Selected = true;
            item.update();

            // BEGIN Select all items in a project - 10/20/2016 SSI EDM
            this.ssi_selectAllProjectItems(treeNode);
            // END Select all items in a project - 10/20/2016 SSI EDM
        }

        treeNode = lastAotSelection.next();
    }

    sysVersionControlTmpItem_ds.executeQuery();
}

Created new method that will loop through the objects in a project and mark them to be checked in if the object is part of the pending object list. If the object being checked in is not a project this logic will be skipped.

/// <summary>
/// method will search for project file for treeNode being checked in
/// if project found it’ll loop through all project treeNodes and mark other pending objects for check in
/// </summary>
/// <param name="_treeNode">
/// object being set for check in
/// </param>
/// <remarks>
/// Mark all project objects for check in - 10/20/2016 SSI EDM
/// </remarks>

private void ssi_selectAllProjectItems(TreeNode _treeNode)
{
    // assign the project name
    ProjName          projName = _treeNode.treeNodeName();
    // get a list of shared projects
    ProjectListNode   list = infolog.projectRootNode().AOTfindChild("@SYS71475");
    ProjectNode       pnProj;
    // search for project being checked in
    ProjectNode       pn = list.AOTfindChild(projName);

    void searchAllObj(projectNode rootNode)
    {
        #TreeNodeSysNodeType
        TreeNode          childNode;
        TreeNodeIterator  rootNodeIterator;

        if (rootNode)
        {
            rootNodeIterator = rootNode.AOTiterator();
            childNode = rootNodeIterator.next();
            // loop through all childNodes of project
            while (childnode)
            {
                if (childNode.treeNodeType().id() == #NT_PROJECT_GROUP)
                {
                    // childNode is a project group, search for next childNode
                    searchAllObj(childNode);
                }
                else
                {
                    // object found, check to see if object is checked out
                    select forupdate item
                        where item.ItemPath == childNode.treeNodePath();

                    if (item)
                    {
                        // object is checked out, mark for check in
                        item.Selected = true;
                        item.update();
                    }
                }

                // load next child
                childNode = rootNodeIterator.next();
            }
        }
    }

    // check if project was found
    if (pn)
    {
        // project found, loop through project treeNodes
        pnProj = pn.loadForInspection();
        searchAllObj(pnProj);
        pnproj.treeNodeRelease();
    }
}

 

 

 

AX Development Standards and Best Practices

$
0
0

Standards and best practices are a great way to bring a development team together to have uniform code and features across Dynamics AX or any software project. Especially with the over-layering capabilities in AX having a set of best practices can be crucial to being successful in a project. Here are some of the standards and best practices we recommend using while doing AX customizations and development.

Establish a source control system

Having a source control system is a very important part to managing code. Not only does it give you security while making changes, but it gives you historical data on changes made. I have used Microsoft TFS (Team Foundation Server) extensively and really like both its issue logging system and Version Control system. I’ve written a couple other blogs around Helpful TFS Queries and TFS Code Repository .

If you are going to use a source control system, make sure you get in the habit of pulling down code or “syncing”. This will keep you up-to-date and keep from conflicts occurring.

Have a task tracking system

Whether it is bugs, features, or changes life is easier if you have a way to track it. There are a lot of great tools out there for tracking this information. I have, again, mainly used TFS. It gives a large number of fields and is customizable. Emails get lost and it is hard to keep track of things that are priority. So having a central system to track tasks will help on any project.

Create a code commenting standard

When doing customizations in AX it is important to comment your code so that when it comes to for upgrades and/or hot fixes the code merge process becomes much simpler.

First, type of comment is the inline comment. Here we are adding code to an existing method. Here you want to wrap your changes with comments.

Ex.:

// BEGIN <Feature/Bug number> <Description> <Date> <Initials>
      <code goes here>
      // END <Feature/Bug number> <Description> <Date> <Initials>

Or:

CustParameters customParams; // <Feature/Bug number> <Description> <Date> <Initials>

This is just one standard, your organization might prefer a different comment structure.

The second type of comment is the XML header comments. It is recommended that you generate these before any public method, but it is also advisable to add it to any method that you create. Not only does it give a good synopsis of what the method does, but it can also help future developers understand when it was created and for what feature/change. The system will generate this for you by simply going to the line above the header (public int …) and typing “///” (without the quotes). This will generate something like this:

/// <summary>
///
/// </summary>
/// <param name=””>
///
/// </param>
/// <remarks>
///
///</remarks>

It is wise to fill this out and put in the <Feature/Bug number> <Description> <Date> <Initials> into the remarks section.

Use the tools available

Microsoft provided some tools to help validate best practices and standards:

Compiler BPs

The most common one is checking for Best Practice violations or “BPs” when compiling.

To do this simply go to (from the Development workspace): Tools > Options > Development.

Click the Compiler button

ax-standards_dave

Set Diagnostics level to “Level 4”

ax-standards_dave2

Doing this will make it so that when you compile any object the BP failures show up in the message log.

ax-standards_dave3

Form Style Checker

Another tool is the Form Style checker. The form style checker will verify a form’s layout to see if matches that of predesigned form templates. This tool is very helpful when a new form is needed in AX. To read more on Form Style Checker, you can read about it in a blog article here.

Code reviews

Having others review your code is always a good idea. Having others review code changes gives different perspectives. This becomes even more beneficial for larger projects. When you are in the depths of coding it is easy to miss things. Having a second pair of eyes can find mistakes or code performance issues.
Doing a code review should be practical and to the point. Point out needed/suggested changes without pointing fingers or blaming. It can be hard at first to have someone review your code as you can feel like you are being judged. Instead, treat it as a chance to learn and grow as a developer.

Overlayering

In general, we (Stoneridge Software) recommend doing development in one layer. CUS and VAR are the 2 more common layers. We do not recommend doing development in the USR layer. In addition, for the most part, it is wise to only have modifications in on model as well. This makes upgrades much easier to work on.

Keep shipped code clean

While doing modifications, try to put as little code into the base objects as possible. Create new helper methods and classes to do any heavy lifting where possibly. This makes it so that the base objects will only have a few lines added/modified. Of course, following the commenting guidelines mentioned above.

Use Labels

Labels are a way to do string replacement in AX. They are very helpful if your company is multi-lingual. In general, if a label exists in the SYS layer it is safe and recommended to use. I would shy away from using labels from ISVs though as they may be a bit more unstable. If unsure, it is always safer to create your own label in your own label file.

Coding standards

Every organization with a development team should have a set of coding standards.

Such as…

Variable declarations:
CustParameters custParameters: (space)
vs
CustParameters		custParameters; (tab)
Comment layout
Brace placement:
if (……) (Microsoft standard)
{
	……….
}
vs.
if (…….) {
	………..
}
Etc.

These are just a few different things to keep in mind while doing Development in AX. There are plenty others as well. If you would like, here is the white paper from Microsoft on this topic: Microsoft Dynamics AX 2012 Best Practices for Developing Customizations White paper.

 

 

Version Control in Dynamics AX: Ensure that code changes are being made in model tied to version control

$
0
0

Recently we ran into an issue where code changes were made in a model that was not tied to version control. When it came time to create a new build these changes weren’t included when the version control sync was executed. We had to take the time to move all of this code from one model to another and restart the build process. After this situation occurred a couple of times I decided to try to find a solution to help prevent this.

What I did was make a change to the class SysVersionControlSystem. This addition takes the version control folders from the XML definition file and compares them to the current model when AX is opened. If the current model doesn’t match a folder tied to version control it will return an error stating to change the model before making code changes. If this message does come up it is best to change the startup model in Tools – Options in the development tab. This is a pretty simple solution but could end up saving a lot of time if code changes are ever made in the incorrect model. It will also catch if a developer opens AX in a layer that is not tied to version control. Code screen shots are below.

NOTE: This was tested using Team Foundation Server. I did not test with Source depot or Visual SourceSafe but think it should work with them. I don’t think this would work with MorphX VCS since it uses AX database tables and doesn’t have a local repository folder.

Code review:
Created new method called ssi_ValidateCurrentModel. In method createModelsMap added call to new method (shown in first screenshot at end of method)

private void createModelsMap()
{
    MapEnumerator mapEnumerator;
    Filename modelFile;
    FilePath modelFilePath;
    FilePath modelFolder;
    SysVersionControlModelFile controllableModelFile;
    SysVersionControlSyncParameters syncParm;
    int line;
    boolean fileExists;

    mapEnumerator = modelFolders.getEnumerator();
    models = new Map(Types::Integer, Types::Class);
    aldLocations = new Map(Types::Integer, Types::String);

    // Iterate the folders
    while (mapEnumerator.moveNext())
    {
        modelFolder = mapEnumerator.currentKey();
        modelFilePath = strFmt(@'%1\%2', this.parmFolder(), modelFolder);
        modelFile = strFmt(@'%1\%2', modelFilePath, #ModelFileNameWithExtension);

        fileExists = System.IO.File::Exists(modelFile);

        // Read the model.xml file in each folder
        if (!fileExists)
        {
            // In case the file is in the VCS but not locally, force sync it
            line = infologLine();

            syncParm = SysVersionControlSyncParameters::construct();
            syncParm.parmSilent(true);
            syncParm.parmForce(true);

            controllableModelFile = SysVersionControlModelFile::construct();
            controllableModelFile.parmModelFolder(modelFile);

            this.commandSynchronize(controllableModelFile, syncParm);

            infolog.clear(line);

            fileExists = System.IO.File::Exists(modelFile);

            // Give warning if it still does not exists after a sync.
            if (!fileExists)
            {
                warning(strFmt("@SYS327410", modelFilePath));
            }
        }

        if (fileExists)
        {
            controllableModelFile = SysVersionControlModelFile::construct();
            controllableModelFile.fromFile(modelFile);
            this.addModelFolder(controllableModelFile);

            if (aldLocationsFromXML.exists(modelFolder))
            {
                aldLocations.insert(controllableModelFile.parmModelId(), aldLocationsFromXML.lookup(modelFolder));
            }
        }

        // BEGIN Version Control Model Check - 02/17/2017 - SSI EDM
        this.ssi_ValidateCurrentModel();
        // END Version Control Model Check - 02/17/2017 - SSI EDM
    }
}

/// <summary>
/// method that checks if startup model is tied to version control
/// </summary>
/// <remarks>
/// Version Control enhancements - 02/17/2017 - SSI EDM
/// </remarks>

private void ssi_ValidateCurrentModel()
{
    // NOTE: below logic was tested with TFS.  it should work with any version control that has a repository folder.
    // Visual SourceSafe and Source depot were not tested.

    boolean     tfsModel = false;
    modelName   tfsModelName;
    FilePath modelFolder;
    MapEnumerator mapEnumerator;
    ModelName   modelName;
    int currentModelId;

    // get current model id and set mapEnumerator
    currentModelId = xInfo::getCurrentModelId();
    mapEnumerator = modelFolders.getEnumerator();
    // remove spaces in folder name if any exists
    modelName = strRem(SysModelStore::modelId2Name(currentModelId), " ");

    // loop through version control folders
    while (mapEnumerator.moveNext())
    {
        modelFolder = mapEnumerator.currentKey();
        // remove spaces in folder name in case it was named with spaces in version control
        tfsModelName = strRem(modelFolder, " ");

        // compare current model name to version control model
        if (modelName == modelFolder)
        {
            // current model is tied to version control, exit loop
            tfsModel = true;
            break;
        }
    }

    // display error message if current model is not tied to version control
    if (!tfsModel)
    {
        //TODO: create label for message
        Error(strFmt("Current Model %1 is not tied to TFS.  Change model to %2 before making code changes", modelName, modelFolder));
    }

}

 

Delegates in Dynamics 365 for Operations

$
0
0

Dynamics 365 for Operations is well on its way to making over-layering of code a thing of the past. The application is moving towards and extension model approach. This approach should allow for a faster reaction to provide and push hotfixes and continuous upgrades. One approach Dynamics 365 for Operations is taking to make the application more extensible is the utilization of delegates. By adding these delegates, they are adding the ability for customers and/or partners to add customizations that can be stored in their own package.

A few things to know about x++ Delegates:

• Cannot have modifier. It is always protected.
• Return type must be “Void”
• Defining method cannot contain code
• Subscribing methods must have the same signature as delegate the delegate definition.

Below are a couple delegate examples for Dynamics 365 for Operations.

Create delegate definition

The following code shows an example of a delegate definition:

delegate void defineDelegate()
    {
    }

Create delegate handler

Open the class in the visual studio designer right click on the delegate definition and click “Copy event handler method” from the menu. The will automatically create the handler method for the delegate.

The following code shows example of a handler method.

[SubscribesTo(classStr(DelegateExample), delegateStr(DelegateExample, defineDelegate))]
    public static void DelegateExample_defineDelegate()
    {
    }

Full code example

Below I have provided a set of classes that show examples of different ways you can use delegates.

There are three classes:
1. DelegateExamples – Contains delegate definitions
2. DelegateExampleSubscriptions – Contains handler methods
3. DelegateExampleRun – Job that will run and product the output

DelegateExamples

/// <summary>
/// Example of delegates. Dynamics for Opertions X++
/// </summary>
class DelegateExamples
{
    /// <summary>
    /// Delegate definition.
    /// </summary>
    /// <param name = "r"></param>
    delegate void addTwoNumbersDelegate(real a, real b)
    {
    }

    /// <summary>
    /// Delegate definition that provides a way to return a result from the delegate handler.
    /// </summary>
    /// <param name = "r"></param>
    delegate void addTwoNumbersReturnResultDelegate(real a, real b, EventHandlerResult result )
    {
    }

    /// <summary>
    /// Call to the addTwoNumbersDelegate delegate, since actual call is protected.
    /// </summary>
    /// <param name = "a"></param>
    /// <param name = "b"></param>
    public void triggerAddTwoNumbersDelegate(real a, real b)
    {
        this.addTwoNumbersDelegate(a,b);
    }

    /// <summary>
    /// Uses delegate in a method which will return a result from the delegates handler.
    /// </summary>
    /// <param name = "a"></param>
    /// <param name = "b"></param>
    public real returnDelegateAdditionResults(real a, real b)
    {
        EventHandlerResult result = new EventHandlerResult();
        real total = 0;

        this.addTwoNumbersReturnResultDelegate(a,b,result);

        total = result.result();

        return total;
    }

}

DelegateExampleSubcriptions

/// <summary>
/// Contains the delegate handler examples to the delegates created in the DelegateExample class.
/// Dynamics for Operations X++
/// </summary>
class DelegateExampleSubscriptions
{

    /// <summary>
    /// Delegate handler. This is where to add your business logic to do something.
    /// For example insert/update values in a table.
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    [SubscribesTo(classStr(DelegateExamples), delegateStr(DelegateExamples, addTwoNumbersDelegate))]
    public static void DelegateExample_addTwoNumbersDelegate(real a, real b)
    {
        real total = 0;

        total = a + b;

        info(strFmt("Invoking delegate addTwoNumbersDelegate: total %1", total));
    }

    /// <summary>
    /// Delegate handler. This will allow you to return a result to the method that
    /// triggered the delegate.
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="result"></param>
    [SubscribesTo(classStr(DelegateExamples), delegateStr(DelegateExamples, addTwoNumbersReturnResultDelegate))]
    public static void DelegateExample_addTwoNumbersReturnResultDelegate(real a, real b, EventHandlerResult result)
    {
        real total = 0;

        total = a + b;
        result.result(total);
    }

}
DelegateExampleRun

/// <summary>
/// Class with main method
/// </summary>
class DelegateExampleRun
{
    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        DelegateExamples dExample = new DelegateExamples();
        real totalResult = 0;

        dExample.triggerAddTwoNumbersDelegate(5,3);

        totalResult = dExample.returnDelegateAdditionResults(1.5,3.5);

        info(strFmt("Invoking delegate returnDelegateAdditionResults: total %1", totalResult));
    }

}

Output

To Run:
• Copy all three classes into visual studio.
• Compile
• Right click on the DelegateExampleRun class and set as start up object as shown below.

• Then click start in visual studio to run

 


Writing to SysException Log when You Have an Outer TTS Scope in Dynamics AX

$
0
0

A customer requested to have an error logged to the SysException table in Dynamics AX when the AIF process experienced an error. The caveat was they did not want the whole AIF process to abort due to this failure and roll back all of the changes for the import, just that one record. To achieve this they needed to capture the error and commit the error message into SysException log. The code still needed to track the failure to the test so it could roll back that change and move on to the next object in the list and continue to process.

Because this code was inside an outer TTS scope, anytime they tried to write the exception to the database it rolled back the exception log due to the failure. To resolve this they needed to add an additional TTS level, but that caused to have a TTS level greater than 1.

To resolve that it was necessary to call the setConnection method and pass in the userConnection. This allowed it to create its own scope for the transaction, commit the insert into the SysException log, and still roll back the changes from the error AIF import object.

TTS Scope

Calculating Customer Aging Balances in X++

$
0
0

I was recently asked to add an Account Summary to the footer of a report in AX 2012, to look something like this:

CustomerAgingBalances_LauraLake

In my research, I came across a helpful class that allowed me to meet this requirement of calculating customer aging balances in X++ with a minimum of coding effort; the CustVendAgingStatistics class. The calcStatistic method queries StatRepInterval and StatRepIntvervalLine to get the buckets and settings for the calculation, then it writes out the values to tmpAccountSum, which is an InMemory temporary table. Depending on what aging buckets you need, you can use a debugger to figure out which tmpAccountSum records you will want.

For my purpose, I created a new method to calculate the customer’s aging balances using a 30D format (#agingFormat macro). I needed open balances (Balance03) and I opted to store both balance and description in a Map. The integer key was helpful so that I could retrieve the values later and add them to the SalesInvoiceHeaderFooterTmp record in SalesInvoiceDP.insertSalesInvoiceHeaderFooterTmp method. You could easily use a container here, depending on how you are using the aging information.

private void SSI_calcAging(CustInvoiceAccount _invoiceAccount)
{
    CustVendTable               custVendTable;
    CustVendAgingStatistics     custVendAgingStats;
    TmpAccountSum               tmpAccountSumLocal;
    int                         i;
    custVendTable  = CustTable::find(_invoiceAccount);

    custVendAgingStats =       CustVendAgingStatistics::construct(custVendTable,#agingFormat,
DateTransactionDuedate::DueDate,True);
    custVendAgingStats.calcStatistic();
    tmpAccountSumLocal.setTmpData(custVendAgingStats.tmpAccountsum());
    i = 0;
    cachedaccountSummary = new Map(Types::Integer,Types::Real);
    cachedaccountSummaryTxt = new Map(Types::Integer,Types::String);
    while select tmpAccountSumLocal
    {
        i++;
        cachedaccountSummary.insert(i,tmpAccountSumLocal.Balance03);
        cachedaccountSummaryTxt.insert(i,tmpAccountSumLocal.Txt);
    }

}

Obviously, this could be used for Vendor Aging as well. I verified that the values match exactly with the Customer Aging report when it is run with the same parameters.

Extension Model: Creating a Static Table Method in Dynamics 365 for Operations

$
0
0

Creating table static methods for some tables was straight forward prior to the release of Microsoft Dynamics 365 for Operations (AX7), and arguably it still is if you are overlaying. Although this article is not intended to distinguish between over layering and extensions, I feel it is important enough to include some key points in this post.

Creating table methods that are static, while using the extension model are a little different. In this scenario, I will create a new method to validate that there is an entry in the external comment field in the table: TSTimeSheetLineWeek.

static class TSTimesheetLineWeek_Extension
{
    /// <summary>
    /// Don't forget your comments
    /// </summary>
    /// <returns>
    /// Don't forget your comments
    /// </returns>
    /// <remarks>
    /// Don't forget your comments
    /// </remarks>
    public static boolean validateExtNotes(TSTimesheetLineWeek _tstTimeSheetLineWeek)
    {
        int i;
        boolean ret = true;

        for(i = 1; i <= 7; i++)
        {
            if (_tstTimeSheetLineWeek.Hours[i] != 0)
            {
                if (_tstTimeSheetLineWeek.ExternalComments[i] == '')
                {
                    ret = checkFailed("External Comments are missing");
                    break;
                }
            }
        }
        return ret;
    }
}

 

NOTE: – I created a class with a name of the table (TsTimeSheetLineWeek) and the mandatory

NOTE:  Parameter of static table method must be of the type of table you are creating the method for, in this case – TsTimeSheetLineWeek.

 

To call the method, you could do something like the following:

class TSTimesheetEntryFormEventHandler
{
    /// <summary>
    /// Don't forget your comments
    /// </summary>
    /// <param name="args"></param>
    [PostHandlerFor(tableStr(TSTimesheetLineWeek), tableMethodStr(TSTimesheetLineWeek, validateRecord))]
    public static void TSTimesheetLineWeek_Post_validateRecord(XppPrePostArgs args)
    {
        boolean                 		validated = true;
        TsTimesheetLineWeek     	timeSheetLineWeek = args.getThis() as TSTimesheetLineWeek;
        TSTimesheetLine         	timeSheetLine;
 
        timeSheetLine = TSTimesheetLine::findRecId(timeSheetLineWeek.TSTimesheetLine);
 
        // check the line property to see if external notes are required.
        if(UtilClass::checkLinePropertyExtNotes(timeSheetLine.LinePropertyId))
        {
           validated = validated & timesheetLineWeek.validateExtNotes();
        }
        // check the line property to see if internal notes are required.
        if(UtilClass::checkLinePropertyIntNotes(timeSheetLine.LinePropertyId))
        {
            validated = validated & timesheetLineWeek.validateIntNotes();
        }
        args.setReturnValue(validated);
    }

Over layering

A few key points to remember about over layering, include:

  • You can customize source code and metadata of model elements that are shipped by Microsoft or third-party Microsoft partners.
  • The overlaying model must belong to the same Package as the source model and belong to a layer that is higher than the source model.
  • Over layering is a powerful tool to perform advanced customizations of metadata and source code, but may increase the cost of upgrading a solution to a new version.

Use of extensions

A few advantages to remember about the use of extensions include:

  • Extension models simplify and improve the performance of deployments, builds, test automation and delivery to customers.
  • Building your model or project doesn’t require you to recompile the entire application.
  • Microsoft can install, patch, upgrade, and change internal APIs without affecting your customizations.
  • Extensions reduce the cost of upgrading to a new version, as this approach eliminates costly code and metadata conflicts.

How to Programmatically Create Queries in Dynamics AX and the Surprises Your Data Can Bring

$
0
0

NOTE: For this article, a vendor has been created with the account number of MyVendor, and an account name of MyVendor, Inc.

There are times when you need to retrieve data out of Dynamics AX that will not allow you to write ‘while select…’ statements.  This is where you can create queries in Dynamics AX, and build the where clause via range values on your query.  This sounds fairly simple, but you have to be aware of what you are retrieving from the database, as you can see some rather unexpected results.

For example, say we wanted to loop through the DirPartyTable to find possible duplicates based on a vendor name.  You want to create something that is basically select * from DirPartyTable where name like ‘your name goes here’

To demonstrate this, I have created the following job (please note that this is for demonstration purposes only and used for help explain the concept I am trying to expand upon):

static void QueryRangeThoughts(Args _args)
{
    Query query;
    QueryBuildDataSource qbds;
    QueryBuildRange qbr;
    QueryRun queryRun;

    Counter cntr = 0;
    Counter vendCnt = 0;
    str myRange = '';

    VendTable vendTable;

    select firstOnly vendTable where vendTable.AccountNum == 'MyVendor';

    if (vendTable)
    {
        // get the name for the search.  We want the portion that exists prior to the first space
        cntr = strFind(vendTable.name(),' ',1,strLen(vendTable.name()));
        myRange = subStr(vendTable.name(),1,cntr - 1 ) + '*';
    }

    query = new query();
    qbds = query.addDataSource(tableNum(DirPartyTable));
    qbr = qbds.addRange(fieldNum(DirPartyTable,Name));
    qbr.value(myRange);

    queryRun = new queryRun(query);
    while (queryRun.next())
    {
        vendCnt++;
    }
    info(strFmt("Vendor count: %1",vendCnt));

}

When this is run, I get the following:

How to create a query in Dynamics AX infolog

Knowing my data, I know that is not correct.  So, lets figure out what is going wrong here.  What query is being generated by my code.  I modified the code to show me:

static void QueryRangeThoughtsDebug(Args _args)
{
    Query query;
    QueryBuildDataSource qbds;
    QueryBuildRange qbr;
    QueryRun queryRun;

    Counter cntr = 0;
    Counter vendCnt = 0;
    str myRange = '';

    VendTable vendTable;

    select firstOnly vendTable where vendTable.AccountNum == 'MyVendor';

    if (vendTable)
    {
        // get the name for the search.  We want the portion that exists prior to the first space
        cntr = strFind(vendTable.name(),' ',1,strLen(vendTable.name()));
        myRange = subStr(vendTable.name(),1,cntr - 1 ) + '*';
    }

    query = new query();
    qbds = query.addDataSource(tableNum(DirPartyTable));
    qbr = qbds.addRange(fieldNum(DirPartyTable,Name));
    qbr.value(myRange);

    info (query.toString());

    queryRun = new queryRun(query);
    while (queryRun.next())
    {
        vendCnt++;
    }
    info(strFmt("Vendor count: %1",vendCnt));

}

The highlighted line is used to show me the generated query in my Infolog box:

Building Queries in Dynamics AX

Looking at what is generated, the where clause is incorrect.  The desired where clause should be myVendor* with NO OR clause.  What is going on?

After walking the code in the debugger, the issue is apparent.  Look at the range value in the debugger:

Building Queries in the Debugger, Microsoft Dynamics AX

Notice the myRange variable value is MyVendor,* in the debugger.  When this is added to the value of the query range the value is interpreted as Name = N’MyVendor’ OR Name LIKE N’*’ not what we want.  The comma character is NOT being removed from the vendor name string prior to the range being created.

The following code shows one way to address this:

static void QueryRangeThoughtsCompleted(Args _args)
{
    Query query;
    QueryBuildDataSource qbds;
    QueryBuildRange qbr;
    QueryRun queryRun;

    Counter cntr = 0;
    Counter vendCnt = 0;
    str myRange = '';

    VendTable vendTable;

    select firstOnly vendTable where vendTable.AccountNum == 'MyVendor';

    if (vendTable)
    {
        // get the name for the search.  We want the portion that exists prior to the first space
        cntr = strFind(vendTable.name(),' ',1,strLen(vendTable.name()));
        myRange = subStr(vendTable.name(),1,cntr - 1 ) + '*';
        myRange = strRem(myRange,',');
    }

    query = new query();
    qbds = query.addDataSource(tableNum(DirPartyTable));
    qbr = qbds.addRange(fieldNum(DirPartyTable,Name));
    qbr.value(myRange);

    info (query.toString());

    queryRun = new queryRun(query);
    while (queryRun.next())
    {
        vendCnt++;
    }
    info(strFmt("Vendor count: %1",vendCnt));

}

The strRem function was used to simply remove the comma from the range string value PRIOR to applying the string as the range.  This creates the query as desired, and gives us the information we expect:

Query object in Dynamics AX

Based on what is in my data, the above is correct, and as we can see in the query, the where condition created is what is desired.

The moral of the story ends up being “know your data”, “plan for the unexpected”, and “ test, test, test” so things like the above don’t randomly start appearing as the data changes.  The possibilities are endless in data, so we have to try to catch what we can so the expected results are shown.

Well, that’s all for now. Happy coding!

Updating the Next Rec ID Sequence in Dynamics AX

$
0
0

Recently I had to create database records outside of Dynamics AX. I used SQL to move some records from ISV tables to new standard tables within AX. There was a ton of data, so it was much faster to insert these records using SQL. Initially, I overlooked one important factor. When I created these records outside of the AX environment, the SystemSequences table was not updated.

The SystemSequences table stores the next RecID by table in the AX environment: https://msdn.microsoft.com/en-us/library/systemsequences.aspx
If this table is not updated properly, the users will see duplicate record errors that may not make sense to you. The users had sent me this error below. When I looked at the data, the Load ID did not exist. It didn’t throw an error about Rec IDs, so it’s a bit misleading.

RecIDSequence_Nicole

The issue was that I had created records in the WHSLoadTable outside of AX.  The SystemSequences table wasn’t updated, so the next value of the Rec ID was incorrect and throwing an error. In this situation, you must update the SystemSequences table. You’ll need to find the max value for the table and update to the max value + 1.

Here is a portion of the SQL used to fix this issue:

DECLARE @MaxRecID BIGINT
 	DECLARE @NextVal BIGINT

SELECT @MaxRecID = MAX(RECID)
FROM WHSLoadTable

SELECT @NextVal = NEXTVAL
	FROM SYSTEMSEQUENCES
	INNER JOIN SQLDICTIONARY
	ON SQLDICTIONARY.FIELDID = 0
	AND SQLDICTIONARY.name = 'WHSLoadTable'
	AND SQLDICTIONARY.TABLEID = SYSTEMSEQUENCES.TABID

	IF (@NextVal > @MaxRecID)
	BEGIN
		PRINT 'WHSLoadTable did not need to be updated.'
	END
	ELSE
	BEGIN
PRINT 'Updated WHSLoadTable from ' + CONVERT(VARCHAR(MAX), @NextVal) + '' to '' + CONVERT(VARCHAR(MAX), @MaxRecID + 1)

		UPDATE SYSTEMSEQUENCES
		SET NEXTVAL = @MaxRecID + 1
		FROM SYSTEMSEQUENCES
		INNER JOIN SQLDICTIONARY
		ON SQLDICTIONARY.FIELDID = 0
		AND SQLDICTIONARY.name = 'WHSLoadTable'
		AND SQLDICTIONARY.TABLEID = SYSTEMSEQUENCES.TABID
END

I hope this helps you out!

Viewing all 71 articles
Browse latest View live