Showing posts with label Utility. Show all posts
Showing posts with label Utility. Show all posts

Sunday, December 9, 2012

Dynamics AX SFTP

Dynamics AX and .NET do not natively support SFTP.  The following approach is one way around that.

We use PuTTY (psftp.exe), which is freely available.

PuTTY download page
http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html

PuTTY psftp.exe documentation
http://the.earth.li/~sgtatham/putty/0.53b/htmldoc/Chapter6.html

We call psftp.exe using command line parameters and, in more complicated cases, a script file written in psftp's scripting language.

Psftp.exe is called from a batch file (.bat) which enables us to respond to prompts that psftp generates and also provide a basic level of error checking.

I did not yet add code to detect that psftp.exe is available in the working directory or the PATH environment variable. If it is not available, the example code will appear to run and give no indication that it failed.

So, here's the code with more specific in-code notes/documentation:

//NDP 12/9/2012 - example job code to connect via SFTP (SSH File Transfer Protocol, aka Secure FTP) and transfer a file from Microsoft Dynamics AX 2012
static void uploadFileWithPSFTP(Args _args)
{
    TextIo                  textIoFile;
    Set                     permissionSet;
    FileName                filePath, filename, batchFileName, scriptFileName, errorThrownFile, errorThrownFileWithPath;
    InteropPermission       interopPerm;

    System.Diagnostics.Process              process;
    System.Diagnostics.ProcessStartInfo     processStartInfo;

    ;
    filePath = 'c:\\Export';                                        //local working folder
    fileName = 'fileToSend.txt';                                    //file existing in local working folder that is to be sent via SFTP

    errorThrownFile = 'ftpError.txt';                               //file used to detect exit codes from psftp
    errorThrownFileWithPath = filePath + '\\' + errorThrownFile;
    batchFileName = filePath + '\\sendfile.bat';                    //batch file used to call psftp.exe
    scriptFileName = filePath + '\\psftp.script';                   //script file used to instruct psftp to follow scripted steps.

    //delete file batch and script file if they exists
    interopPerm = new InteropPermission(InteropKind::ClrInterop);
    interopPerm.assert();

    if(System.IO.File::Exists(batchFileName))
    {
        System.IO.File::Delete(batchFileName);
    }
    if(System.IO.File::Exists(scriptFileName))
    {
        System.IO.File::Delete(scriptFileName);
    }

    CodeAccessPermission::revertAssert();

    //write the batch file
    permissionSet = new Set(Types::Class);
    permissionSet.add(new FileIoPermission(batchFileName, "W"));
    CodeAccessPermission::assertMultiple(permissionSet);

    textIoFile = new TextIo(batchFileName , "W", 0);

    CodeAccessPermission::revertAssert();

    //the 'echo n|' in the line below will answer 'no' to the question that psftp will ask to trust/store the certificate from the ftp server.
    textIoFile.write(strFmt('echo n|psftp ftpaddress.com -P 23 -l username -pw password -b %1',scriptFileName));

    //this creates an empty file if the exit code/errorlevel from psftp is 1 (meaning there was an error)
    textIoFile.write(strFmt('IF ERRORLEVEL 1 echo. 2>%1',errorThrownFile));

    textIoFile = null;

    //write the script file that psftp uses
    permissionSet = new Set(Types::Class);
    permissionSet.add(new FileIoPermission(scriptFileName, "W"));
    CodeAccessPermission::assertMultiple(permissionSet);

    textIoFile = new TextIo(scriptFileName , "W", 0);

    CodeAccessPermission::revertAssert();

    textIoFile.write('cd /subfolder');               //change folders on the FTP server, optional
    textIoFile.write(strFmt('put %1',fileName));     //send the file
    textIoFile.write('quit');

    textIoFile = null;

    //run the batch file
    new InteropPermission(InteropKind::ClrInterop).assert();

    process = new System.Diagnostics.Process();
    processStartInfo = new System.Diagnostics.ProcessStartInfo();
    processStartInfo.set_FileName(batchFileName);
    processStartInfo.set_WorkingDirectory(filePath);
    process.set_StartInfo(processStartInfo);

    process.Start();
    process.WaitForExit();

    //process.get_ExitCode() does not seem to be implemented in AX 2012.  Instead we're using the 'errorthrownfile' method above.

    CodeAccessPermission::revertAssert();

    interopPerm = new InteropPermission(InteropKind::ClrInterop);
    interopPerm.assert();

    //delete the files that were used to facilitate the transfer
    if(System.IO.File::Exists(batchFileName))
    {
        System.IO.File::Delete(batchFileName);
    }
    if(System.IO.File::Exists(scriptFileName))
    {
        System.IO.File::Delete(scriptFileName);
    }

    //determinte if an error was thrown by psftp.  Clean up the error indicator file.
    if(System.IO.File::Exists(errorThrownFileWithPath))
    {
        warning ("Error occured during SFTP transfer");
        System.IO.File::Delete(errorThrownFileWithPath);
    }
    else
    {
        info("SFTP transfer was successful");
    }

    //delete the file that was sent - you might not want to do this in your application
    if(System.IO.File::Exists(fileName))
    {
        System.IO.File::Delete(fileName);
    }

    CodeAccessPermission::revertAssert();
}


Have fun!

Tuesday, July 17, 2012

Excel Helper class for AX

I often find it useful to give users the ability to export and import defined-format Excel files throughout AX.  For example, I'll put Export and Import buttons on the CustTable form that allows the user to do quick mass-updates of some set of fields that often needs updating.  There are more out-of-the-box ways to do this, but you all know that sometimes a simple bit of custom code goes a long way in satisfying a user.

The sysExcel* classes (SysExcelApplication, SysExcelWorkbooks, SysExcelWorkbook, SysExcelWorkSheets, SysExcelWorkSheet, SysExcelCells, Com, COMVariant) are fairly straight forward to use, but since I'm usually using the same few basic functions, I took the time to create a little helper class.

The ExcelHelper class is available for you to download at the end of this post.  The example code below is also included in the class.

Imports
I might know a file name, but more often I just want to display a File Open dialog box for the user to select their file.  I usually want to include some basic instructions about what kind of file they can import (i.e. what columns).  Below is some example import code using the ExcelHelper class:

private static void exampleOfUseForImport()
{
    ExcelHelper                 ExcelHelper;
    str                         dialogText;
    int                         lastRow, row;
    exampleTable                exampleTable;
    int                         recCount;
    ;
    ExcelHelper = new ExcelHelper();

    dialogText = strfmt("Excel file with no header row.   Columns: Customer Account, Item Id, Quantity, Ship Date, Purchase Order";

    if (ExcelHelper.openExcelFile("",dialogText))  //when no file name is passed, an open file dialog box will be displayed
    {
        global::startLengthyOperation();

        lastRow = ExcelHelper.findLastRow();

        for (row = 1; row <= lastRow; row++)
        {
            exampleTable.clear();
            exampleTable.CustAccount = ExcelHelper.readValue(row,1);
            exampleTable.ItemId = ExcelHelper.readValue(row,2);
            exampleTable.Qty = str2num(ExcelHelper.readValue(row,3));
            if (ExcelHelper.readValue(row,4) != "")
            {
                exampleTable.ShipDate= str2date(ExcelHelper.readValue(row,4),123);
            }
            exampleTable.PurchaseOrder = ExcelHelper.readValue(row,5);
            exampleTable.insert();
            recCount++;
        }
        global::endLengthyOperation();

        ExcelHelper.closeExcelFile();

        box::info(strfmt("%1 records inserted",recCount),"Import Results");
    }
}

Exports
To make exports as easy to use as possible, I just open Excel with the exported data in an unsaved worksheet.  The user can then use the standard Excel menus to save the file.  Below is some example export code using the ExcelHelper class:
private static void exampleOfUseForExport(CustInvoiceAccount _invoiceAccount)
{
    custTable                   custTable;
    ExcelHelper                 ExcelHelper;
    int                         row;
    ;
    ExcelHelper = new ExcelHelper();
    ExcelHelper.openBlankExcelFile();

    ExcelHelper.formatAsText('A:A');

    ExcelHelper.setValue(1,1,"Customer Account");
    ExcelHelper.setValue(1,2,"Customer Name");

    row = 2;

    global::startLengthyOperation();

    while select custTable
        where custTable.InvoiceAccount == _invoiceAccount
    {
        ExcelHelper.setValue(row,1,custTable.AccountNum);
        ExcelHelper.setValue(row,2,custTable.Name);
        row ++;
    }

    global::endLengthyOperation();

    ExcelHelper.showExcel();
}

There are plenty of more advanced things that you can do with the SysExcel classes, but the stuff that I've included in the ExcelHelper class seems adequate for the majority of my needs.

Here is the class:
Class_ExcelHelper.xpo (google docs - shortened via goo.gl - 9kb)

There are a series of blog posts on Excel that I've found helpful.  Here is one:
http://patrikluca.blogspot.com/2010/04/export-to-excel-with-x-code-sequel.html

Wednesday, January 18, 2012

Run-time add/remove datasource to filter results

Sometimes I add a Combobox above a Grid to give the user several 'canned' filters.
In some cases I would like to filter on a table other than the one displayed in the grid.  For example, I might want to filter a grid of CustTable records based on related Address table records.

If you want the filter to be active all the time, you can just add the filtering datasource to your form.  But if you only want the filter to be active some of the time, you need to be able to dynamically (run-time) add and remove the datasource that does the filtering.

The code below shows how to dynamically add and remove (actually disable) a child datasource for filtering purposes.

public void executeQuery() //on form datasource "formDatasourceName"
{
    QueryBuildDataSource    qbds;
    ;
 
    if(this.query().dataSourceName("formDatasourceName").childDataSourceCount() == 1)
    {
        //if the filter datasource has been added in the past (prior run of executeQuery), disable it
        this.query().dataSourceName("formDatasourceName").childDataSourceNo(1).enabled(false);
    }

    if (booleanFilterResults)
    {
        //this creates a "hidden" exists join to act as a filter
        if (this.query().dataSourceName("formDatasourceName").childDataSourceCount() == 1)
        {
            //if the filter datasource
            qbds = this.query().dataSourceName("formDatasourceName").childDataSourceNo(1);
            qbds.enabled(true);
        }
        else
        {
            qbds = this.query().dataSourceName("formDatasourceName").addDataSource(tablenum(filterTableName));
        }

        qbds.clearRanges();

        qbds.relations(true);  //or use .addLink...
        qbds.joinMode(JoinMode::ExistsJoin);
        qbds.fetchMode(queryfetchmode::One2One);
        
        //filter on some value, in this case a comboBox selection.  
        //Of course you'll call formDatasourceName.executeQuery() in comboBoxFilter modified method
        qbds.addRange(fieldnum(filterTableName, ItemId)).value(queryValue(comboBoxFilter.valueStr()));
    }

    super();
}

Wednesday, July 14, 2010

Price Unit Excel Calculation

Here's the Excel calculation that I use when determining what Price Unit to use during imports/conversions to avoid loss of accuracy on small unit prices.

Price Unit =POWER(10,MAX(LEN(A2) - FIND(".",A2) -2,0))
Price = A2*Price Unit
(Where A2 is the original price, i.e. $0.0045)

Everyone loves that Price Unit!

Monday, February 22, 2010

Image Resource can not be saved in temporary folder

We recently added a custom image to a report.  To do this, we added a resource (AOT, Resources node) and put the following code in the executeSection method:
MyReportBitmapControl.imageName(SysResource::getImagePath("MyResourceName"));
This worked find until we ran the report from a Menu Item.  Then we started to get an error that said "unable to save the data to c:\...\Temporary Internet Files\myResource.bmp".

The issue ended up being that the Menu Item was set to Run On: Server.  When the Menu Item was set to Run On: Client, the problem was fixed.

Wednesday, February 17, 2010

Reset TTS Level to zero

When I executed an unbalanced transaction block, I use this code to get back to zero:
static void NDPToolsResetTTS(Args _args)
{
    while (appl.ttsLevel() > 0)
    {
        info(strfmt("Level %1 aborted",appl.ttsLevel()));
        ttsAbort;
    }
}

Find TableName from TableId

Sometimes I see a tableId in the data (i.e. in a refTableId field) and I wonder what table that is.

I use this job to find it:
static void NDPToolsShowTableNameFromId(Args _args)
{
    SysDictTable sysDictTable;
    int          id = 40015; //enter your ID here
    ;
    sysDictTable = new SysDictTable(id);
    info (sysDictTable.name());
}

Friday, February 12, 2010

Date and Time in DAX

Just a quick example of the basic date and time functions.
static void time(Args _args)
{
    int                     t,h,m,s;
    Date                    d;
    InteropPermission       perm;
    System.DateTime         dttime;
;

    //method #1
    t = timenow();
    d = systemDateGet(); //preferred to today()
    h = trunc(t/3600);
    m = trunc((t - h*3600)/60);
    s = t - h*3600 - m * 60;
    info (strfmt("%1 %2:%3:%4",d,h,strRFix(int2str(m),2,"0"),s));

    //method #2
    perm = new InteropPermission(InteropKind::ClrInterop);
    perm.assert();
    dttime = CLRInterop::staticInvoke("System.DateTime","get_Now" );
    CodeAccessPermission::revertAssert();
    info (dttime.ToString());
}
 Notes:
  • the DateTime type has the full array of .NET methods (i.e. ToShortTimeString, ToUniversalTime, etc)
  • systemDateGet() is affected by systemDateSet, making it superior in testing scenarios
  • strRFix is used to pad zeros onto single-digit minutes (i.e. 10:4 -> 10:04)

Friday, January 29, 2010

Bitmaps in Reports

One way to add 'custom' images in an AX report is to:
  1. Add the image as a resource in the AOT, Resources node (right click, Create from File)
  2. In the report, Add a Bitmap control and set AutoDeclaration to Yes.
  3. In the report, sometime prior to the rendering of the bitmap (i.e. executeSection prior to super), code:
myBitmapControl.imageName(SysResource::getImagePath("myresourcename"));

Thursday, January 14, 2010

SQL Query to Move Definition Groups

It's not possible to use Definition Groups (Admin,Periodic,Data Export/Import, Definition Groups) to move data held in shared tables.  So, here's the quick and dirty sql query that I use (in SQL Server Management Studio) to do the job.
DECLARE @GroupId AS varchar(20)

SET @GroupId = 'FA1' --set this to your Definition Group ID

--change all 8 database names to match your environments
INSERT INTO AX42_DB_TEST.dbo.sysExpImpGroup SELECT * FROM AX42_DB_DEV.dbo.sysExpImpGroup WHERE groupId = @GroupId
INSERT INTO AX42_DB_TEST.dbo.sysExpImpTable SELECT * FROM AX42_DB_DEV.dbo.sysExpImpTable WHERE groupId = @GroupId
INSERT INTO AX42_DB_TEST.dbo.sysExpImpField SELECT * FROM AX42_DB_DEV.dbo.sysExpImpField WHERE groupId = @GroupId
INSERT INTO AX42_DB_TEST.dbo.sysExpImpTableQuery SELECT * FROM AX42_DB_DEV.dbo.sysExpImpTableQuery WHERE groupId = @GroupId
I've also read that when you use standard Data Export/Import move a definition group itself, CRs are stripped out of your conversion code. (http://gotdax.blogspot.com/2009/12/dynamics-ax-import-export-tool-digging.html) This gets around that problem as well.

Tuesday, January 12, 2010

Data Import Conversion

The 'trick' to placing code in the conversion tab of a Definition Group is to define a second table buffer and using the .data() method to copy the date back and forth.

Tuesday, December 15, 2009

Tools

There are several tools that I find useful almost every day:

Gadwin Printscreen
http://www.gadwin.com/printscreen/
There is a free version.  I set it to capture a 'Rectagular Area' to the clipboard.  Wonderful.

Cute PDF
http://www.cutepdf.com/
A free PDF writer that's always worked well for me.

Dynamics AX Task Recorder
Available from MS PartnerSource.  An amazingly easy way to create user documenation for AX.

Dynamics AX 2009 Keyboard Shortcuts
http://kashperuk.blogspot.com/2008/07/microsoft-dynamics-ax-2009-keyboard.html
I'm not sure who wrote this document, but it was a big help to me.

Format source code for blog posting
http://formatmysourcecode.blogspot.com/
Just right.

Multiple AOS Environment

Recently I've been working on a site with 3 production AOSs.  Code updates were a hassle because we would have to restart each AOS after importing the new code.  The following post shares a method to solve this problem and it's worked great for us.

http://dynamics-ax.blogspot.com/2006/04/flush-aos-cache-from-code.html