Wednesday, December 1, 2010

Copying a Production Database into a Development/Test Environment

Disclaimer: In no way am I making claim that this is the best, correct, or accepted approach to take when bringing down a copy of Production into Development. The outlined method has worked for me and served my purposes well.. with that said, follow my steps at your own risk!


1. Stop the AOS on Development or Test
2. Backup the AX Database in Production, and Restore it into Development or Test
3. Restore all the Security permissions/mappings to the AX Database (Model them after Production)
4. Write a T-SQL script to replace Production Server & User names in the Database with that which are related to Development or Test (see below for tables/columns that need to be changed)
5. On Development or Test, delete all files in the application folder (C:\Program Files\Microsoft Dynamics AX\50\Application\Appl\[Your AX Instance]) where the file extension ends with the letter “i”
6. Start the AOS on Development or Test (this may take awhile)
7. Open the AOT, right-click the Data Dictionary, and select Synchronize


Tables in the AX Database that need to be Modified (see step #4 above)

I had written a utility that mined the database for all references to Production system names. The following is the script that I came up with to “convert” a Production Database to Development/Test. Note: not all changes may actually be necessary (such as in the log tables) but for consistency, I change them anyway.

Declare @AOS varchar(15)
Declare @SP varchar(15)
Declare @SQL varchar(15)
Declare @axbcp varchar(15)

Set @AOS = '[Your DEV AOS Server Name]'
Set @SP = '[Your DEV SharePoint/Enterprise Portal Server Name]'
Set @SQL = '[Your DEV SQL Server Name]'
Set @axbcp = '[Your DEV axbcp User Account Name]'

Declare @P_AOS varchar(15)
Declare @P_SP varchar(15)
Declare @P_SQL varchar(15)
Declare @P_axbcp varchar(15)

Set @P_AOS = '[Your PRODUCTION AOS Server Name]'
Set @P_SP = '[Your PRODUCTION SharePoint/Enterprise Portal Server Name]'
Set @P_SQL = '[Your PRODUCTION SQL Server Name]'
Set @P_axbcp = '[Your PRODUCTION axbcp User Account Name]'


----------------------------------------------------------------------------------
Update [SYSBCPROXYUSERACCOUNT] Set NETWORKALIAS = @axbcp WHERE NETWORKALIAS = @P_axbcp
----------------------------------------------------------------------------------


Update [BATCH] Set SERVERID = REPLACE(upper(SERVERID), upper(@P_AOS), upper(@AOS))
Where upper(SERVERID) like '%' + upper(@P_AOS) + '%'
-----------------------------------------------------------------------------------


Update [BATCHHISTORY] Set SERVERID = REPLACE(upper(SERVERID), upper(@P_AOS), upper(@AOS))
Where upper(SERVERID) like '%' + upper(@P_AOS) + '%'
-----------------------------------------------------------------------------------


Update [BATCHSERVERGROUP] Set SERVERID = REPLACE(upper(SERVERID), upper(@P_AOS), upper(@AOS))
Where upper(SERVERID) like '%' + upper(@P_AOS) + '%'
------------------------------------------------------------------------------------


Update [EPWEBSITEPARAMETERS] Set [INTERNALURL] = REPLACE(upper([INTERNALURL]), upper(@P_SP), upper(@SP))
Where upper([INTERNALURL]) like '%' + upper(@P_SP) + '%'
Update [EPWEBSITEPARAMETERS] Set [EXTERNALURL] = REPLACE(upper([EXTERNALURL]), upper(@P_SP), upper(@SP))
Where upper([EXTERNALURL]) like '%' + upper(@P_SP) + '%'
------------------------------------------------------------------------------------


Update SRSSERVERS Set [SERVERURL] = REPLACE(upper([SERVERURL]), upper(@P_SQL), upper(@SQL))
Where upper([SERVERURL]) like '%' + upper(@P_SQL) + '%'
Update SRSSERVERS Set [AXAPTAREPORTFOLDER] = REPLACE(upper([AXAPTAREPORTFOLDER]), upper(@P_SQL), upper(@SQL))
Where upper([AXAPTAREPORTFOLDER]) like '%' + upper(@P_SQL) + '%'
Update SRSSERVERS Set [REPORTMANAGERURL] = REPLACE(upper([REPORTMANAGERURL]), upper(@P_SQL), upper(@SQL))
Where upper([REPORTMANAGERURL]) like '%' + upper(@P_SQL) + '%'
------------------------------------------------------------------------------------


Update [SYSCLIENTSESSIONS] Set CLIENTCOMPUTER = REPLACE(upper(CLIENTCOMPUTER), upper(@P_AOS), upper(@AOS))
Where upper(CLIENTCOMPUTER) like '%' + upper(@P_AOS) + '%'
Update [SYSCLIENTSESSIONS] Set CLIENTCOMPUTER = REPLACE(upper(CLIENTCOMPUTER), upper(@P_SP), upper(@SP))
Where upper(CLIENTCOMPUTER) like '%' + upper(@P_SP) + '%'
Update [SYSCLIENTSESSIONS] Set CLIENTCOMPUTER = REPLACE(upper(CLIENTCOMPUTER), upper(@P_SQL), upper(@SQL))
Where upper(CLIENTCOMPUTER) like '%' + upper(@P_SQL) + '%'
------------------------------------------------------------------------------------


Update [SYSEMAILSMTPPASSWORD] Set [AOSID] = REPLACE(upper([AOSID]), upper(@P_AOS), upper(@AOS))
Where upper([AOSID]) like '%' + upper(@P_AOS) + '%'
------------------------------------------------------------------------------------


Update [SYSLASTVALUE] Set [DESIGNNAME] = REPLACE(upper([DESIGNNAME]), upper(@P_AOS), upper(@AOS))
Where upper([DESIGNNAME]) like '%' + upper(@P_AOS) + '%'
Update [SYSLASTVALUE] Set [DESIGNNAME] = REPLACE(upper([DESIGNNAME]), upper(@P_SQL), upper(@SQL))
Where upper([DESIGNNAME]) like '%' + upper(@P_SQL) + '%'
Update [SYSLASTVALUE] Set [DESIGNNAME] = REPLACE(upper([DESIGNNAME]), upper(@P_SP), upper(@SP))
Where upper([DESIGNNAME]) like '%' + upper(@P_SP) + '%'
------------------------------------------------------------------------------------


Update SYSSERVERCONFIG Set [SERVERID] = REPLACE(upper([SERVERID]), upper(@P_AOS), upper(@AOS))
Where upper([SERVERID]) like '%' + upper(@P_AOS) + '%'
------------------------------------------------------------------------------------


Update SYSSERVERSESSIONS Set [AOSID] = REPLACE(upper([AOSID]), upper(@P_AOS), upper(@AOS))
Where upper([AOSID]) like '%' + upper(@P_AOS) + '%'
------------------------------------------------------------------------------------


Update SYSUSERLOG Set [COMPUTERNAME] = REPLACE(upper([COMPUTERNAME]), upper(@P_AOS), upper(@AOS))
Where upper([COMPUTERNAME]) like '%' + upper(@P_AOS) + '%'
Update SYSUSERLOG Set [COMPUTERNAME] = REPLACE(upper([COMPUTERNAME]), upper(@P_SP), upper(@SP))
Where upper([COMPUTERNAME]) like '%' + upper(@P_SP) + '%'
Update SYSUSERLOG Set [COMPUTERNAME] = REPLACE(upper([COMPUTERNAME]), upper(@P_SQL), upper(@SQL))
Where upper([COMPUTERNAME]) like '%' + upper(@P_SQL) + '%'
------------------------------------------------------------------------------------


Update SYSWORKFLOWPARAMETERS Set [SITEURL] = REPLACE(upper([SITEURL]), upper(@P_SP), upper(@SP))
Where upper([SITEURL]) like '%' + upper(@P_SP) + '%'
------------------------------------------------------------------------------------



Re-Syncing Enterprise Portal

Unfortunately, by restoring a Production Database into Development/Test, it appears to break connectivity with Enterprise Portal. As of now, the only way I have figured out how to resolve this issue is by doing the following:

Verify Your SharePoint Administrative Access

1. START > Administrative Tools > SharePoint Central Administration
2. Go to Application Management > SharePoint Site Management > Site Collection Administrations
3. Click the Site Collection drop down > Change Site Collection
4. Click the "/sites/DynamicsAx" URL and click OK
5. Make sure your name is listed as either the Primary or Secondary site collection administrator. NOTE: if you are not the Primary or Secondary site collection administrator, you will get an "Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)" error when attempting to install Enterprise Portal

Reinstall Enterprise Portal

1. Load the Setup.exe (run as Administrator) from your Dynamics AX 2009 installation path
2. Select “Add or modify components”
3. Select “Role Centers and Enterprise Portal” and click Next
4. Verify that the “Domain\user name” is correct for your environment (if not make the change to the NETWORKALIAS column in the SYSBCPROXYUSERACCOUNT table in the AX database, and then restart the setup) and enter in the password for the account
5. Select the existing Dynamics AX EP website
6. Check the “Configure for Windows SharePoint Services” checkbox
7. Check the “Create Web site” checkbox
8. Verify the Site URL
9. Click Next
10. Click “Yes” to allow the setup to delete and recreate
11. Click Install

Run a command prompt (run as Administrator) and enter the following command lines:

1. cd c:\Program Files\Microsoft Dynamics AX\50\Setup
2. AxUpdatePortal –updateWebSites –verbose > “C:\EPUpdate.log”
3. iisreset

Enterprise Portal should once again be functioning. At this point you will need to go into your EP site, and add your user permissions again.

Thursday, September 2, 2010

CLR Errors & ttsbegin/ttscommit Blocks

  
Recently I had modified the smmCampaignBroadcast class to call a custom class/method that I had used and modified, to send emails using CLR via the System.Net.Mail assembly (which was being used elsewhere... see my blog on Sending Emails From Dynamics AX Without Outlook). It worked great, until a certain a condition caused the Send() function to fail. The problem came in to play with despite the fact that I had try/catch blocks around core pieces code in the function (meant to alert the user of errors) in this particular scenario, the process was simply taking a dump, and the error causing the problem was not being caught. Upon stepping through the code with the debugger, immediately after hitting the problem line of code (which was the Send() function as mentioned above) the debugger stepped into a \Classes\Application\ttsNotifyAbort() method, which basically aborted the entire process, not allowing my try/catch block around the Send() call to acknowledge the error.

Upon some research, it appears that the problem is related to levels of ttsbegin/ttscommit blocks, and apparently if a CLR error occurs inside of a ttsbegin/ttscommit block, you cannot trap the error until you are outside the block (and in the case of nested ttsbegin/ttscommit blocks, you must be outside of the highest level).

Using the Stack Trace in the debugger, I analyzed the code, to identify where the first ttsbegin/ttscommit block was, and then wrapped that piece of code with a try/catch block meant to catch a CLR error. So, in the smmCampaignBroadcast::broadcast() method I made the following changes:

try
{


ttsbegin;


…….


ttscommit;


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

Finally, I was able to see what was going wrong!

Friday, July 30, 2010

Making Modifications to Report Libraries in AX and Re-Deploying the Latest Versions to SSRS

   
Creating and initially deploying a Report Library into SSRS is quite easy. However when it came time to make a modification to that Report Library, I quickly found out that my latest version was not actually deploying, because changes were not actually being made to my Report Library in the AOD, despite the fact that Visual Studio was reporting success with a message such as: “Saving project [Project Name] and all its items to AOD succeeded”. The problem appears to be that when clicking “Save to AOD” in Visual Studio, for a project that already exists in AX, the project isn’t actually overwritten. To resolve this, follow these simple steps when making a mod to a Report Library.

1. Right-Click the Report Library that you want to edit, and select “Edit in Visual Studio”
2. If you are prompted with an overwrite prompt, click “Yes to All”
3. Make your modifications
4. When ready to save back to AX, select: File > Save All
5. Go back into AX and Delete the Report Library that you are editing
6. Go back into Visual Studio, right-click the Project, and select “Save to AOD”
7. Go back into AX, right-click the Report Libraries node in the AOT, and select “Refresh” and you will see your Report has returned, now being your most recent version.

Other things to keep in mind

If your Report Library is the defined Object of a Menu Item, and there is a chance that you may have modified the Name of the Report while editing it, make sure that you check the Object value of the Menu Item to verify that it is valid (otherwise, the Menu Item will point to the previous version of the report in SSRS).

If you Import the edited Report Library into another environment, make sure that you Delete the existing Report Library first. I have found that the Import does not always overwrite the existing Report Library, even during an import.
   

Thursday, July 29, 2010

Sending Emails From Dynamics AX without Outlook

  
Recently we had a need to have Dynamics AX send emails without the use of Outlook. I was presented with the following blog by Mohammed Rasheed titled Sending Emails From Dynamics AX without Outlook, which proved to be extremely useful. There was however an issue with the code (perhaps simply related to our environment) which was puzzling. The problem was that the email would send successfully, but afterward, an error was thrown stating:

ClrObject static method invocation error

The line of code that appeared to be the culprit was basically the last line in the function:

CodeAccessPermission::revertAssert();

However, after analyzing things more closely the problematic line was actually the line just above:

winApi::deleteFile(fileNameforEmail); // delete temp file

As it turns out, the deleteFile() function was failing because the file had not been released by a previous process. Though it was a bit painful to figure out what was going on, the solution itself was rather simple.

Immediately after sending the email, I modified the code to dispose the objects that may have been clinging to the file

mymail.Send(mailmessage);


mailmessage.Dispose();
attachment.Dispose();

After this, the file was released, and was able to be deleted, and the error went away!
  

Monday, June 28, 2010

Retrieving Record Values in an Enterprise Portal Wizard/Tunnel

    
Let’s say you have a scenario in which you need to pull a field from a newly created record that was inserted into AX via an Enterprise Portal Wizard/Tunnel. This need could be useful in the event that after a record is created, you need to obtain the record’s Id, so that you can do something else which relates to the new record. In this example we will extend the code example found in the book “Microsoft Dynamics AX 2009 Programming: Getting Started” (page 304)

protected void WizardTunnel_FinishButtonClick(object sender,
WizardNavigationEventArgs e)
{
AxDataSourceView rentalTableView =
this.RentalDataSet.GetDataSourceView("RentalTable");


if (rentalTableView != null)
rentalTableView.EndEdit();


AxUrlMenuItem carRentalsMenuItem =
new AxUrlMenuItem("CarRentalList");
Response.Redirect(carRentalsMenuItem.Url.ToString());
}
To pull the value from the CarRentalId in the following modified event handler…

protected void WizardTunnel_FinishButtonClick(object sender,
WizardNavigationEventArgs e)
{
AxDataSourceView rentalTableView =
this.RentalDataSet.GetDataSourceView("RentalTable");


if (rentalTableView != null)
{


DataSetViewRow dvr = rentalTableView.DataSetView.GetCurrent();
string CarRentalId = dvr.GetFieldValue("CarRentalId").ToString();


rentalTableView.EndEdit();
}


AxUrlMenuItem carRentalsMenuItem =
new AxUrlMenuItem("CarRentalList");
Response.Redirect(carRentalsMenuItem.Url.ToString());
}
    

Thursday, June 24, 2010

Resolving one cause of the "Invalid datasource name" in Enterprise Portal

  
If you have developed a Web Control for Enterprise Portal, which contains an AxDataSource, and an AxForm, and then you attempt to include an AxLookup control you may receive the following error:

Invalid datasource name ‘[Your Data Source Name Here]’
However, you have verified that the data source name is most definitely correct and is indeed part of your Data Set. The problem may be that you have put your AxLookup inside the bounds of the AxForm control. Place your AxLookup control outside of the AxForm, and your problem will probably go away (unless of course it is being caused by another issue)
  

Wednesday, June 23, 2010

Importing AX Projects (.XPO) & Deploying Web Controls into Enterprise Portal: Resolution for Controls not showing in the “Managed content item” drop down.

       
Recently I seriously struggled for some time in exporting Web Controls in the AOT in my development environment, and importing them into the staging environment. I created a project to include all of my web controls, by adding the related web control nodes from [AOT > Web > Web Files > Web Controls]. I then imported this project into staging, and went into Enterprise Portal to setup my Web Part Page with my custom web controls in the AOT. The problem however was that when modifying my shared web part (a Dynamics User Control Web Part), the “Managed content item” drop down did not contain my custom web controls. Oddly enough, I could create a new project in Visual Studio, and select the “Add user control from AOT” option, and my custom controls were in the list. So what exactly was going on??

A veteran Axapta developer, whom I have the privilege of occasionally tapping for information if all else fails, revealed to me that Enterprise Portal does not pull its list of Web Controls from the [AOT > Web > Web Files > Web Controls] node, like Visual Studio does, but rather, in order to see your controls in the “Managed content item” drop down, you must include the references to the controls that are found in the [AOT > Web > Web Content > Managed] node. So when I added these references to my project in the development environment, exported the project, and then imported the project into staging, and then restarting both the AOS and IIS for good measure… My custom web controls finally showed up and could be used.
       

Resolving the “System.NullReferenceException: Object reference not set to an instance of an object” on a standard Enterprise Portal page

     
Recently we were running stress tests on various interfaces that came standard in Enterprise Portal. In particular, we were testing out the Purchase > Purchase requisitions interface. Everything worked until we clicked the New > New purchase requisition button. After that, things blew up and we got a really nasty error that began with the following:

An unexpected error has occurred.


Microsoft.Dynamics.Framework.BusinessConnector.Session.Exceptions.FatalSessionException: Dynamics Object Adapter Call failed. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Dynamics.Framework.Data.Ax.DataSetViewRow.GetFieldValue(DataSetViewFieldMetadata field)
at System.Web.UI.WebControls.BoundField.GetValue(Control controlContainer)
at Microsoft.Dynamics.Framework.Portal.UI.WebControls.AxBoundField.OnDataBindField(Object sender, EventArgs e)
at System.Web.UI.Control.OnDataBinding(EventArgs e)
at System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding)
at System.Web.UI.Control.DataBindChildren()

Long story short, I eventually found the solution thanks to a post made by someone else who was having a similar issue, and double-thanks to the same person who took the time to come back and post the solution when he/she found it. This post can be seen here.

The problem was that I had recently added a new Dimension to the system. There was however one step that I did not know I had to do after adding the dimension and doing a synchronize. The missing final step was to re-compile the “Data Sets” node in the AOT. After doing this, and then restarting IIS on the Enterprise Portal Machine, the error went away, and problem was solved!
  

Thursday, June 17, 2010

What is the “Record write validation failed. DataKey” error?

        
During Enterprise Portal Development, I received this error in a Tunnel/Wizard that I put together to create a new record in a custom table. I originally googled this error when I first received it, but absolutely no results were returned. Not too long after, a little debugging revealed that the problem was that I was not supplying the value for a mandatory (Number Sequence) field.

If I come across other causes for the problem, I will append them to this blog as I find them.

 

Monday, June 14, 2010

Working with Proxies and the Ambiguous “No .NET Business Connector session could be found” or “Cannot access a disposed object” Errors

Recently, I created an X++ class with a series of methods that I wanted to be able to access via a Web Part in Enterprise Portal. Microsoft’s documentation on Proxies provided a good basis for setting this up.

The problem however came in using the following using block to wrap the code calling the X++ method:

Everything worked great, that was until I used the same block in two different events (called during the same page load) to call another X++ method. It wasn’t immediately clear what was going on, because my page started throwing the ambiguous error “No .NET Business Connector session could be found”, which led me to believe that I either broke something in my code, or that Enterprise Portal was acting quirky.
using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{

}


As I would eventually find out, attempting to obtain a new session, for example, by using the helper function (below) more than once during a given page load, is where the trouble resides. The first time you get the session, everything is great; however the second time, the session is returned as null, which effectively causes the problem.
private ISession AxSession
{
get
{
AxBaseWebPart webpart = AxBaseWebPart.GetWebpart(this);
return webpart == null ? null : webpart.Session;
}
}

The simplest way to setup this error-producing scenario is to call this using block two times in a row, using similar code as seen below. Everything works great stepping through the first block, but the page blows up during the second.


protected void Page_Load(object sender, EventArgs e)
{


using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{
MyCustomClass test = new MyCustomClass(a);
string s = test.myCustomFunction();
}


using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{
MyCustomClass test = new MyCustomClass(a);
string s = test.myCustomFunction();
}


}
Even more ugly is the error that is thrown if you attempt to use the using code block on a web control that has an AxDataSource, this error actually reveals the root problem behind the previous error.

[ObjectDisposedException: Cannot access a disposed object.]

The solution to both errors is rather simple. Setup your IAxaptaAdapter as a global variable in your class, and establish the session only one time during your Page_Load event.

public partial class myClass : System.Web.UI.UserControl
{
private IAxaptaAdapter axapta;


private ISession AxSession
{
get
{
AxBaseWebPart webpart = AxBaseWebPart.GetWebpart(this);
return webpart == null ? null : webpart.Session;
}
}


protected void Page_Load(object sender, EventArgs e)
{
axapta = this.AxSession.AxaptaAdapter;

You will then be able to access the session multiple times, for example, you could call your X++ method in the page load event, and later in the Button1_Click event.


protected void Page_Load(object sender, EventArgs e)
{
axapta = this.AxSession.AxaptaAdapter;


MyCustomClass test = new MyCustomClass(axapta);
string s = test.myCustomFunction();


}


protected void Button1_Click(object sender, EventArgs e)
{
Tmt_CallReportClass test = new Tmt_CallReportClass(axapta);
string s = test.myCustomFunction();
}

This will also allow you to have a AxDataSource control on the page, without any issues.

Friday, June 11, 2010

Open a URL in a Browser from X++

Here is an easy way, using CLR, to load a URL in the client’s default browser via X++, from say, a button’s clicked event:


void clicked()
{
System.Diagnostics.Process pr = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo si = new System.Diagnostics.ProcessStartInfo('URL HERE');
pr.set_StartInfo(si);
pr.Start();
}

A Web Service Reference Regeneration Quirk

    
I had modified a function in a .NET Web Service while adding an additional function to it. The Web Service reference had been previously added to the AOT, so I only needed to refresh the reference, in order for my changes (the additional funtion) to be “seen” by the AX development environment.

After publishing my modified Web Service to the web server, I located my reference to it within the AOT, right-clicked it, and selected “Regenerate”. After getting the usual expected slew of errors, that ultimately reveal the need to restart the AOS so that the new assemblies can be loaded, I did just that. However that’s when things got strange. First, when I attempted to test my method in AX which calls the web service, I received the following error:

Type 'System.ServiceModel.Channels.ReceivedFault' in assembly 'System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' is not marked as serializable.
I verified that the function was a set to run on the server (public static server [return type] [method name])

I then recompiled my method, and received a “… does not contain this function” error, pointing to the call to my modified web service function, that I had previously been successfully calling in AX. I also verified that the new function I had created was not being seen by AX either. This was strange, because, I verified the function was indeed part of the service, by accessing it directly via the web browser. After doing several “regenerates” and restarts on the AOS, all to no avail, left me scratching my head. So I decided to try one more thing.

I had been executing the “regenerate” option on the service reference, via my the AX client application on my local machine. So this final time, I remoted into the machine where the AOS was actually running, and loaded up the client from there. I then executed the “regenerate” option again, and restarted the AOS one more time. To my pleasant surprise, my mysteriously missing web service function (and the new function I created) were suddenly visible in AX, and I could then execute my method call without receiving the System.ServiceModel.Channels.ReceivedFault error.

The so the moral of the story seems to be the following:

When you want to refresh your web service reference in AX, do it from the client application running on the same machine as the AOS!
      

Thursday, May 20, 2010

Enterprise Portal: Adding an AX Web Control and Resolving the “unable to load content” Error

Following the tutorial on creating a web part page for Enterprise Portal in the book “Microsoft Dynamics AX 2009 Programming: Getting Started” by Erlend Dalen (Great book by the way!) things seemed simple enough from creating a dataset in AX (page 290) to creating the web part in Visual Studio (pages 291-295). Even the first steps of creating the web part page was straight forward (296-299). However, because my development environment was in 64-bit Windows, when it came to modifying my shared web part, to use the web part that I built in Visual Studio, one additional key step was needed. At first I received the following error message:


'unable to load content [webcontrol name]. Please contact your systems administrator'

Finding Out What The Actual Problem Is

In order to get to the bottom of what was actually going on, I learned that I needed to modify my main web.config file which is located at:

C:\Inetpub\wwwroot\wss\VirtualDirectories\80

to give me a more useful message.

1) First, I needed to modify the following line that begins with:

SafeMode MaxControls=”200” CallStack=”false” …

And change the CallStack property to “true

2) Next, I needed to modify the following line:


customErrors mode=”On” /

And change the mode property to “Off

3) Next, I needed to modify the following line:

compilation batch="false" debug="false"

And change the debug property to "true"

4) I then saved the the web.config file
5) Restarted IIS by executing the IISRESET command in the command prompt
6) Went through the steps of creating the web part page again.
7) I then received a far more useful error, with the first line of the error telling the story:


System.Web.HttpException: The file '/_layouts/ep/[webcontrol name].ascx' does not exist.

The Missing Step

Now knowing that the problem was that my control files didn’t actually exist where SharePoint expected them, I needed to locate the exact path where these files belonged. As it turns out, after searching, the “/_layouts/ep” path is not a physical path on the machine, so I had to find its physical location some other way. How I did this was searching the hard drive for another AX web part that was in the list of webparts that I verified was able to load (so I searched for “ActivitiesListAssociations”). This led me to the physical path of:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\ep

As expected, my custom web control files were not there.

So I copied the .ascx and .ascx.cs files from my project to this path, went through the steps to create the web part page, and web part finally loaded!

Tuesday, May 18, 2010

Setting Default Values on a New Record in a Grid

Today is another newbie quick-tip. Setting default values on a new record in a Grid is a simple task. To do this, you need to override the initValue() method on the Form's Data Source. Let say I have a Table named tblDefault which contains two fields TheDate, which is a UtcDateTime type, and Who, which is a String type. In my example, I want the TheDate field to default to the current date time, and the Who field to default to the User Id of the current user:

public void initValue()
{
;
super();


// I’ll use the custom function to get the current date/time from here
tblDefault.TheDate = this.Now();
// AX has a built in function to get the current User Id
tblDefault.Who = curUserId();
}

Wednesday, May 5, 2010

Debugger Isn’t Working in the clicked() Method

  
Today I found that something in one of my form button clicked() methods was not doing what I expected it to do. So I put a breakpoint on the particular line of code in the method so that I could execute the process and see exactly what was going on. However, execution was not halting at my breakpoint, and the debugger was never being triggered. After making sure I was attempting to debug the right code, restarting my AX Client, then my machine, and finally the AOS itself, all to no avail; I quickly realized something buggy was going on. A little digging led me here, which acknowledged this as being a known bug in AX. The most basic solution turns out to be simple, and requires the use of the “breakpoint” keyword in the code where you want the breakpoint.

The only problem is that you will want to make sure you remove these breakpoint keywords when you are done!
void clicked()
{
;
breakpoint;
}

 

Tuesday, May 4, 2010

A Now() or GetDate() Method for AX

It should be noted, that after I made this post, it was revealed to me that the DateTimeUtil::utcNow() method accomplishes this goal...

Today I simply have a quick tip. I needed to be able to obtain the current date & time; something similar to the DateTime.Now() function in .NET or the getdate() function in SQL Server; However as far as I can tell, AX provides no quick function for this, so I wrote my own:


public utcdatetime Now()
{


str tDate;
str tTime;
utcdatetime utc3;


;


tDate = date2str(systemDateGet(), 321, DateDay::Digits2, DateSeparator::Hyphen, DateMonth::Digits2, DateSeparator::Hyphen, DateYear::Digits4);
tTime = time2Str(TimeNow(), TimeSeparator::Colon, TimeFormat::AMPM);
return str2datetime(tDate + " " + tTime, 321);


}

Thursday, April 29, 2010

Retrieving Control Values from an Existing Form Instantiated in X++

  
In one of my recent requirements, I needed to instantiate the smmEmailDistribution Form from a custom form that I had created, once control was returned to the custom form, then I needed to pull the values of various elements that the user had modified on the smmEmailDistribution form. How this is done was not immediately clear, and after spending hours attempting to find an example of this type of functionality in my reference books and via general googling, I eventually made a post on the Dynamics User Group forum. Oddly enough however, shortly after posting my question (less than an hour), I discovered the answer on my own by good old trial-and-error. The code below was placed in the clicked() method of a button on my custom Form, and does exactly what my need required:


void clicked()
{


FormStringControl strEdit;
Args args;
FormRun formRun;


;


args = new args();


args.name(formstr(smmEmailDistribution));
formRun = classFactory.formRunClass(Args);
formRun.init();
formrun.run();
formrun.wait();


// Get the Email1 control from smmEmailDistribution
strEdit = formrun.design().controlName("Email1");


super();


}

Tuesday, April 27, 2010

Obtaining the Next Number Sequence via X++

Recently, I needed write a method that would create a line in a custom table for each customer residing in a temporary table (which was actually part of the final process that I briefly mentioned in my last post “Programmatically Inserting Data into a Grid”)

My actual custom Table contained a field that was based on a Number Sequence. Thus in my process, when I programmatically inserted a new record into my custom Table, per customer, I needed to generate the next number sequence for each record. A little digging led me here which revealed some of the code that I was looking for.

On the clicked() method of my “Process” button I essentially did the following:


void clicked()
{
tbl_Complaint complaintReport;


NumberSequenceReference nsr;
NumberSeq ns;
;


while select temp
{
ttsbegin;


complaintReport.CustNumber = tmt.CustNumber;
complaintReport.Name = tmt.Name;


nsr = tbl_Complaint::numRefComplaintId();
ns = NumberSeq::newGetNum(nsr, false, true);


complaintReport.ComplaintReportId = ns.num();


complaintReport.insert();


ttscommit;


}

super();
}
It should be noted, that in my initial run of the code, I did not include the ttsbegin and ttscommit lines. This however threw the following error:

“System does not support setup 'continuous' of number sequence”
Once again, after doing a little digging, it was here that I learned of my options to resolve this error, which for my needs was solved by implementing transaction scope.

Monday, April 26, 2010

Programmatically Inserting Data into a Grid

During many .NET based projects, I have utilized DataGrids in both Windows Form and Web based applications to hold temporary non-bound data used primarily for user verification purposes. More recently, I was faced with a similar requirement in a needed AX modification, but due to “limitations” of the AX Grid control, I was forced to re-think my approach.

The problem: there does not appear to be any way to programmatically add data to a grid; and if there actually is, I have yet to find it. So it seems, that grids must always be bound to a data source. The way around this so-called “limitation”, is to utilize a Temporary Table. A Temporary Table would be used because, unlike standard tables, it does not map to the a relational database, and will only contain data when an active record buffer is attached to it. Temporary record buffers are isolated to the user, thus if two different users are accessing the Temporary Table, they are actually pointed to different datasets. Therefore, if User (A) and User (B) are using the same form at the same time, there would not be a conflict in data integrity.

In my required modification, a form was needed to allow a user to choose from a list of customers, giving them the ability to add their selections to a grid. Once the user verified that the gird contained all of the customers they required, they could then process this list with a click of the mouse. For this particular tutorial, I will explain the basic method that I have discovered to accomplishing this task of “programmatically inserting data into a grid”.

Let’s say we want a Grid to hold a selected list of Customer Names & Numbers. What we would want to do, is create a Table in the AOT, and mark it as Temporary, containing two string fields; Name, CustNumber. We would then want to set the Data Source of our Grid to this Table, and then make a few X++ modifications

Do the following in the AOT:
1. Create a new Table, and name it TempCustList
2. Add two string fields: CustNumber, and Name
3. Right-Click the Table, and select Properties
4. Set the Temporary property to “Yes”
5. Create a new Form, and name it FrmCustList
6. Drag-and-Drop the TempCustList Table into the Form’s Data Sources
7. Expand down to the Form’s Design node.
8. Right-Click the Design node and select: New ControlGrid
9. Right-Click the Grid, and select Properties
10. Set the Name property to “GridCustList
11. Set the Data Source property to “TempCustList
12. Perform the following:

In the FrmCustList Form where we will be using the Temporary Table, we will want to declare the record buffer so that we can access the data in the temporary table anywhere in the form without losing it (as long as our Form remains open). For example, If you declare the Temporary Table within a mouse click method on a form’s button for example, and insert data into the Temporary Table, the data will be lost, as soon as the method completes.

public class FormRun extends ObjectRun
{
TempCustList temp;
}
13. On the Form, add a Button and override it’s click() method with the following code:

void clicked()
{

// Test data, replace with a query to pull actual customer info, based on selection…
temp.Name = "Joe Smith";
temp.CustNumber = "CUST-0001";

// Insert into the Temp table
temp.insert();

// Refresh the Data Source, which then refreshes the Grid…
TempCustList.setTmpData(temp);
TempCustList_ds.executeQuery();

super();
}
The above code, will obviously add the same line to the Grid every time it is clicked, however the scope of this tutorial was simply to show you how to add data to a grid “programmatically”. A simple modification to the code would allow you to utilize the example for your needs.

Wednesday, April 21, 2010

Creating a new Number Sequence for an Existing Module, and Putting it to Use!

When I first learned how to create a Number Sequence in my Dynamics AX 2009 Development IV Class (80014) in February of 2010, I left with more confusion about the topic than it should have, and I dreaded coming back to work with the prospect of actually having to implement new Number Sequences. However, once I pulled together the precise steps from various books, online documents, and flat out trial-and-error, the task turned out to not be quite as bad as I had originally left the class thinking. I was however dumbfounded as to why no single source (that I was able to find) outlined the exact steps from creating a new Number Sequence to putting it to use in one clean tutorial.

NOTE: Please bear with me in this tutorial; I am going to break naming convention best practices, so that my example is clearer.

The first thing you need to do is determine which module you want/need to create the new number sequence for. For this example, I am going create a new number sequence for the “Accounts Receivable” module. Then take the following steps:

Creating a new Number Sequence for the Accounts Receivable Module

1. Create your Extended Data Type that will be used in your table as your Number Sequence field. For this example we will create a string based Extended Data Type called edt_ComplaintId (with a Label property = “Complaint Report”)

2. Next, since we are using the “Accounts Receivable” module, we must modify the proper NumberSeqReference_XXXX Class. Accounts Receivable, actually maps to Customer (or Cust), so we need to make a modification to the NumberSeqReference_Customer class.
We are only concerned with the loadModule() method, and will simply need to add the following code to it:

numRef.dataTypeId = typeId2ExtendedTypeId(typeid(edt_ComplaintId)); numRef.referenceHelp = "Unique key for the Complaint Report"; numRef.wizardContinuous = true;
numRef.wizardManual = NoYes::No;
numRef.wizardAllowChangeDown = NoYes::No;
numRef.wizardAllowChangeUp = NoYes::No;
numRef.wizardHighest = 999999;
this.create(numRef);
3. After compiling and saving your changes, the next step is to use the Number Sequence Wizard to set it up. Click Basic > Setup > Number sequences > Number sequences to open the form where you can manage your Number Sequences.

4. Click on the “Wizardbutton, which then should initialize the wizard.

NOTE: If you receive an error at this point telling you “The application already has the required number sequences”, this means that you either did not add the definition to an established NumberSeqReference_XXXX Class' (in this example, the NumberSeqReference_Customer Class) loadModule() method, or you have already run the Wizard and set it up.
5. Once you arrive on the Wizard Welcome screen, click the Next > button.

6. On the following screen, verify that the Module is “Accounts receivable” and the Reference is “Complaint Report” (which is the label of our Extended Data Type). The Number Sequence code is just an auto-generated value that you can leave as is (Remember this code, so that you can find it in your list of Number Sequences, because you are going to want to make a couple changes to it). Once you have verified the above, click the Next > button.

7. The next screen just gives you an overview, click the Finish button.

8. You should then see your Number Sequence in the list.

9. You will probably want to change the Format to something such as “CR-######”

10. At this point, your Number Sequence is ready for use!


Using the Number Sequence

1. Create a Table and name it tbl_Complaint.

2. Add the edt_ComplaintId Extended Data Type as one of the fields (drag and drop the Extended Data Type directly into the fields of the Table) and name this field ComplaintId.

3. Add the following Method to the tbl_Complaint:

static client server NumberSequenceReference numRefComplaintId()
{
return NumberSeqReference::findReference(typeId2ExtendedTypeId(typeid(edt_ComplaintId)));
}
4. Create a Form called frm_Complaint.

5. Add the tbl_Complaint table to the form’s Data Sources, and name the Data Source ds_Complaint.

6. Add the following Methods to the Form:

public class FormRun extends ObjectRun
{
NumberSeqFormHandler numberSeqFormHandler;
}


NumberSeqFormHandler numberSeqFormHandler()
{
if (!numberSeqFormHandler)
{
numberSeqFormHandler = NumberSeqFormHandler::newForm(Tmt_CallReport::numRefComplaintId().NumberSequence, element, ds_Complaint.dataSource(), fieldnum(tbl_Complaint, ComplaintId));
}
return numberSeqFormHandler;
}
7. Add the following Methods to the ds_Complaint Data Source:

public void create(boolean _append = false)
{
element.numberSeqFormHandler().formMethodDataSourceCreatePre();
super(_append);
element.numberSeqFormHandler().formMethodDataSourceCreate();
}


public void delete()
{
element.numberSeqFormHandler().formMethodDataSourceDelete();
super();
}


public void write()
{
super();
element.numberSeqFormHandler().formMethodDataSourceWrite();
}
8. Your Number Sequence should now function!

Friday, April 9, 2010

Solving: “Error in Url property error”

My latest task was to create a new Role Center for Dynamics AX 2009; which in its most basic form, is simply a webpage on the SharePoint Enterprise Portal that is linked in AX. After creating my Web Part page in the SharePoint EP, I went back to the AX Client to setup the Role Center.

In AX, I did the following steps:

1. Opened the AOT
2. Scrolled down and expanded the “Web” node
3. Expanded the “Web Menu Items” node
4. Right-Clicked the “URLs” node
5. Clicked “New URL” menu item
6. Right-Clicked the new URL item, and selected the “Properties” menu item
7. Filled in the Name and Label properties
8. Clicked the […] button for the URL property

At this point I received an odd error message:

Error in Url property error

After a bit of digging it turns out that a Microsoft patch is required called “Software Update for Web Folders (KB907306)”. This needs to be installed on the machine where the AX client is running. After downloading, and installing the patch, I restarted the AX client, got back to my URL item in the AOT, and the […] button then functioned properly.

Wednesday, March 24, 2010

Corrupted Database? Think Again! It Might Simply Be a Corrupted Layer (aod) File.

Recently we had what appeared to be a major issue occur on our Development AX environment. After performing a Synchronize on the Data Directory in the AOT, we received several warning messages, and then when attempting to view various forms we were receiving SQL errors, and what appeared to be missing data.

The initial assumption was that the Database had become corrupted. So we went through the process of doing a backup of the database in our Testing AX environment (which had no issues), and restoring it to the Development AX environment. After the restore however, we still received the same errors as before, which had us puzzled for a moment. Then my colleague, Tim Golisch, remembered something very important.

Not everything in AX is stored in the Database, in fact, everything relating to the layers, are actually stored in AOD files, which reside in the: C:\Program Files\Microsoft Dynamics AX\50\Application\Appl\[DB NAME] folder.

So I stopped the AOS, removed the axuser.aod file, started the AOS, opened the client, and wala! Everything was back to normal.

Monday, March 22, 2010

Binding Table Contents to a Drop Down List in a Grid with MorphX: Displaying Name & Description

In my last post, I demonstrated how to “bind” a Grid field to a table in drop down list. The example demonstrated how to list the Name column, as defined by the Relation in the Extended Data Type; however, in most cases, the list will need to not only display the Name column, but the Description as well. I actually struggled with how this was accomplished, that was, until an expert AX developer pointed me in the right direction. It turns out, given all the places within the AOT where you can see and set various properties to accomplish a desired task (or totally dork something up), the solution is extremely simple. When viewing the Properties on the table being “bound” to, there are two key properties called TitleField1 and TitleField2. Setting TitleField1 = Name and TitleField2 = Description solves this problem, and both the Name and Description data displays in the drop down list.

Thursday, March 18, 2010

Binding Table Contents to a Drop Down List in a Grid with MorphX

Let’s say you have a custom table called SiteList, with two fields: Name and Description, and you want the list of the Name field in this table to appear in a drop down list in a Grid on a Form. Ultimately, achieving this functionality revolves around the use of an Extended Data Type, and setting it up with the proper Relation.

What you need to do is create an Extended Data Type that will be used as the type for your Name field in your SiteList table, and will also be used in the table that will hold the related data (we will build a custom table called SiteVisitors for this example). After you have created your Extended Data Type, and your Table that will be used for the drop down contents, you then need to create a Relation on your Extended Data Type to link it to your Table. After this, things happen on their own, and when this field is used in a Grid (from the SiteVisitors table for example) the Drop Down will automatically appear (there is no need to define the field in the Grid as a ComboBox).

For an example on setting up this scenario do the following:

1. Create an Extended Data Type called: SiteName, extending the String base type.
2. Create a Table called: SiteList
3. Add a String Field to the SiteList (Table) called: Description
4. Drag and Drop the SiteName (Extended Data Type) into the Fields of the SiteList (Table)
5. Go back to the SiteName (Extended Data Type) and expand its nodes.
6. Right-Click the Relations node, and select New > Normal
7. Right-Click the newly created “SiteName == .” node, and select Properties
8. Set the Table property to: SiteList
9. Set the RelatedField property to: SiteName
10. Create a Table called: SiteVisitors
11. Drag and Drop the SiteName (Extended Data Type) into the Fields of the SiteVisitors (Table)
12. Add a new String field to SiteVisitors (Table) called: VisitorName
13. Add a new Date field to SiteVisitors (Table) called: VisitDate
14. Create a new Form called: SiteVisitors
15. Drag and Drop the SiteVisitors (Table) into the Data Sources of the SiteVisitors (Form)
16. Expand the Designs node of the SiteVisitors (Form) and right-click the Design node, and select New Control > Grid
17. Right-Click the Grid, and select Properties
18. Set the DataSource property to SiteVisitors
19. Go back up to the Data Sources node and expand it, then expand the SiteVisitors node, then expand the Fields node.
20. Select the VisitorName, VisitorDate, SiteName fields, and drag and drop them into the Grid.
21. You should now be able to Open the SiteVisitors Form to see things working (of course you might want to put some data into your SiteList Table first).

The image below shows what your project should look like after following the above steps.



Tuesday, March 16, 2010

Using the .NET Business Connector to Export Sales Orders with Multiple Sales Lines into AX from a .NET Application: Part 2

Expanding on the previous blog, let’s say that another requirement has been added, which is, to add the ability to track the date that a record was imported into the system, which will be used to prevent duplicate records for a given customer in a given month from being imported.

The first step would be to add an ImportDate field in the SalesTable via the AOT

The second step would then be to modify our class (ImportToAX) method (ImportSalesOrder) to accept an import date.

The third step would then be to modify our class (ImportToAX) method (ImportSalesOrder) to include logic to determine if a duplicate record (a given customer in a given month) already exists. Programming instinct coupled with my novice skills in AX development tell me that in order to determine if we already have a record for a given customer in a given month, I would add the following lines into our X++ class (ImportToAX) method (ImportSalesOrder):



Select count(RecId) From salesTable
where salesTable.CustAccount == CustAccount && mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate);
ret = salesTable.RecId;

if (ret == 0)
{
………
}
else
{
throw Global::error("Only one record is allowed per customer in a given month!");
}


This however is invalid code, which throws two errors when compiled. The first being:

Illegal use of WHERE expression

This error is thrown, apparently because methods cannot be used in a where clause, and since I have attempted to use the mthofyr() and year() methods within my where clause, we have an issue. Therefore, one possible way of getting around this, is to take a different approach, such as the following valid code:

found = false;

while select salesTable where salesTable.CustAccount == CustAccount
{
if (mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate)) found = true;
}

if (!found)
{
………
}
else
{
throw Global::error("Only one record is allowed per customer in a given month!");
}

The second error that we get:

Container and unbounded string (text) fields are not allowed in a WHERE expression

This is inherently related to our method definition, where the CustAccount parameter is defined as an str:

str ImportSalesOrder(str CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{
……
}

The problem is that the salesTable.CustAccount field is actually defined as the extended data type, CustAccount, which extends the base type str. Because AX is a bit finicky, a standard str type cannot be compared to an extended data type (even though the extended data type extends the base type we are comparing to). Therefore, the simplest solution is to make our incoming CustAccount parameter of type CustAccount.

str ImportRoyaltyReport(CustAccount CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{
……
}


This actually has no ill affect the .NET side of the project. In fact, the only parts of our .NET function needing change would be first in the function definition:

static public bool ImportOrder(string CustomerAccount, decimal Total, decimal Tax, decimal Fees, DateTime ImportDate)

And then the code to pass in the additional Import Date parameter to the AX method call:

object[] param = new object[5];
param[0] = CustomerAccount;
param[1] = Total;
param[2] = Tax;
param[3] = Fees;
param[4] = ImportDate;
Object retval = Import.Call("ImportSalesOrder", param);


Finally, to reduce code redundancy in our AX class (ImportToAX) method (ImportSalesOrder), the method could be compacted to avoid having the same block of code repeated 3 times (as in Part 1). Here we can use a couple additional variables, a loop, and a switch statement, which will transform our method as whole to look like the following:

str ImportSalesOrder(CustAccount CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{

SalesTable salesTable;
SalesLine salesLine;
InventDim inventDim;
NumberSeq NumberSeq;

str retval;
int line;
str line_item;
real line_amt;
boolean found;
;

retval = "";

found = false;

while select salesTable where salesTable.CustAccount == CustAccount
{
if (mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate)) found = true;
}

if (!found)
{

// Generate the next SalesId (which is a Number Sequence)...

NumberSeq = NumberSeq::newGetNumFromCode(SalesParameters::numRefSalesId().numberSequence);
salesTable.SalesId = NumberSeq.num();

// Create the Sales Order...

salesTable.initValue();
salesTable.CustAccount = CustAccount;
salesTable.deliveryDate = today();
salesTable.PurchOrderFormNum = "Imported Data";
salesTable.Tmt_RoyaltyImportDate = ImportDate;

salesTable.initFromCustTable();

salesTable.insert();

// Create the 3 Sales Lines...

for (line = 1; line <= 3; line++) { switch(line) { case 1: line_item = "Total"; line_amt = Total; break; case 2: line_item = "Taxes"; line_amt = Taxes; break; case 3: line_item = "Fees"; line_amt = Fees; } salesLine.clear(); salesLine.SalesId = salesTable.SalesId; inventDim = salesLine.inventDim(); inventDim.InventSiteId = "01"; salesLine.setInventDimIdFromInventDim(inventDim); salesLine.ItemId = line_item; salesLine.SalesQty = 1; salesLine.SalesUnit = 'EA'; salesLine.SalesPrice = line_amt; salesLine.LineAmount = line_amt; salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No); } retval = salesTable.SalesId; } else { throw Global::error("Only one record is allowed per customer in a given month!"); } return retval; }

Friday, March 12, 2010

Using the .NET Business Connector to Export Sales Orders with Multiple Sales Lines into AX from a .NET Application: Part 1

One requirement that had been given to me was the ability for an existing .NET application to be enhanced to interface with AX. The existing application’s sole purpose was to decode and parse messages being sent to a specified email account, and to export data from these messages into an database. Since the old database and its parent application was going to be slowly phased out with the implementation of AX, the exporting application would need to be enhanced to be able to export key fields of these messages into AX as a Sales Order with multiple Sales Lines. After some thought, I concluded that the solution to this requirement was rather simple in concept, and would need to include an AX Class with one method (used to do the import into the SalesTable and SalesLine tables), as well as an upgrade to the .NET application, utilizing the .NET Business Connector (used to call and execute the AX method).

The basis for my AX method was inspired by a German Dynamics AX Blog, with a title that roughly translates to “Microsoft Dynamics AX API - Part 1: Creating Orders” , by Mathias Füßler. This gave me a good starting point for how to programmatically insert Sales Orders with a related Sales Line. As expected, I needed to make some modifications to the example code for it to work in Dynamics AX 2009, as well as to make it to do exactly what I needed it to do.

The first part of my derived solution for example, was to create a Class in AX, which we will call ImportToAX. This class would contain one key method that we will call ImportSalesOrder. Each Sales Order required a Customer Account number, and three Sales Lines including Total, Taxes, Fees; therefore, the ImportSalesOrder method should accept these required pieces of information as base type parameters.

// This is the only method in the ImportToAX Class
str ImportSalesOrder (str CustAccount, real Total, real Taxes, real Fees)
{

SalesTable salesTable;
SalesLine salesLine;
InventDim inventDim;
NumberSeq NumberSeq;
str retval;
;

retval = "";

try
{

// Obtain the next SalesId (Next Number Sequence)...

NumberSeq = NumberSeq::newGetNumFromCode(SalesParameters::numRefSalesId().numberSequence);
salesTable.SalesId = NumberSeq.num();

// Create the Sales Order...

salesTable.initValue();
salesTable.CustAccount = CustAccount;
salesTable.deliveryDate = today();
salesTable.PurchOrderFormNum = "Imported Data";

salesTable.initFromCustTable();

salesTable.insert();

// Create the first Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;

// The following three lines were added, because AX was requiring a defined site for inventory dimensions…
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);

salesLine.ItemId = ' Total ';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Total;
salesLine.LineAmount = Total;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

// Create the second Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);
salesLine.ItemId = 'Taxes';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Taxes;
salesLine.LineAmount = Taxes;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

// Create the third Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);
salesLine.ItemId = 'Fees';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Fees;
salesLine.LineAmount = Fees;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

retval = salesTable.SalesId;
}
catch
{
retval = "";
}

return retval;
}


The second part of the solution was to create a .NET Class utilizing the .NET Business Connector (which requires a reference to C:\Program Files\Microsoft Dynamics Ax\50\Client\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll). The following .NET Class (C#) contains one core function which is used simply to connect to Dynamics AX, and then to execute the AX method with its required parameters:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Dynamics.BusinessConnectorNet;

namespace ImportToAXExample
{
public class ImportToAXClass
{

static public bool ImportOrder(string CustomerAccount, decimal Total, decimal Tax, decimal Fees)
{
Axapta ax;

bool Success = true;

try
{
ax = new Axapta();
ax.Logon(null, null, null, null);

AxaptaObject Import;
Import = ax.CreateAxaptaObject("ImportToAX");

object[] param = new object[4];
param[0] = CustomerAccount;
param[1] = Total;
param[2] = Tax;
param[3] = Fees;
Object retval = Import.Call("ImportSalesOrder", param);

if (retval.ToString() == "")
{
Success = false;
}

ax.Logoff();
}
catch()
{
Success = false;
}

return Success;
}

}
}

To tie it all together, this .NET Class could then be added to the existing .NET Application, and its only function, ImportOrder, could then be called from the application for each of the records that the application processes. Each time this function is called, required records within the SalesTable and SalesLine tables are created in AX.