Tuesday, March 31, 2015

AX2012 JSON 2 XML converter


While writing some code for the Exchange Rate Provider class, I ran across an issue where some exchange rate provider APIs only gave JSON output instead of XML.  I found very little in Dynamics AX that would let me deserialize JSON.  The only alternative was to manually parse the data based on the structure provided at the time.

I didn't like that approach.  Manually parsing data like that can be a nightmare.  The only tools I know of in Dynamics AX are the XML tools.  With that in mind, I decided to write a JSON to XML converter.

classdeclaration
class JSON2XML
{
BinData binData;
}
view raw gistfile1.cs hosted with ❤ by GitHub


loadFromFile
//load the JSON data from a file
public str loadFromFile(FilenameOpen _file)
{
FileIOPermission _perm = new FileIOPermission(_file,"r");
binData = new binData();
_perm.assert();
binData.loadFile(_file);
return binData.getStrData();
}
view raw gistfile1.cs hosted with ❤ by GitHub


convert2xml
//rudimentry method for converting JSON to XML
public str convert2xml(str _json)
{
#define.SPACE(32)
//remove whitespace from JSON string
str stripSpace(str _str)
{
int i;
str tmpStr;
boolean inquote;
for(i=1;i<=strLen(_str);i++)
{
if (subStr(_str,i,1) == '"')
{
inquote=true;
}else
{
inquote = false;
}
if (char2num(_str,i) <= #SPACE && !inquote)
{
continue;
}
tmpStr = tmpStr + subStr(_str,i,1);
}
return tmpStr;
}
//add header to xml
xml = '<?xml version="1.0" encoding="UTF-8"?>';
xml = xml + "<root>";
//convert
this.convert("root",stripSpace(_json));
return xml;
}
view raw gistfile1.cs hosted with ❤ by GitHub

convert
//recursive method to parse JSON string and construct XML.
container convert(str _tag,str _str)
{
int i = strLen(_str);
str tmpStr = _str;
str tagStr,origTagStr;
str convStr;
int pos;
int quotePos;
int retval;
boolean expand;
boolean inquote;
boolean invalue;
pos = 1;
while (pos<=i)
{
switch(subStr(tmpStr,pos,1))
{
//assign end tag and pop if not in quotes
case ',':
if (!inquote)
{
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos- quotePos);
invalue = false;
}
xml = xml + "</" + _tag + ">";
return [pos,true];
}
pos +=1;
break;
//beginning of a tag if not in quotes, push json element, value
case '{':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos - quotePos);
inValue = false;
}
pos +=1;
if (!inquote)
{
[tagStr,origTagStr] = this.getTag(subStr(tmpStr,pos,strLen(tmpStr)-pos));
xml = xml + "<" + tagStr + ">";
pos += strLen(origTagStr) + 2;
convStr = subStr(tmpStr,pos,strLen(tmpStr)-(pos-1));
[retval,expand] = this.convert(tagStr,convStr);
pos += retval;
//if we are going to expand, then poke a '{' to force another nesting tag
if (expand)
{
pos -=1;
tmpStr = strPoke(tmpStr,"{",pos);
}
}
break;
//ending of the tag if not in quotes, pop json element
case '}':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos -quotePos);
invalue = false;
}
//pos +=1;
if (!inquote)
{
xml = xml + "</" + _tag + ">";
}
return [pos,false];
//begin array if not in quote and push json element, value pairs
case '[':
if (invalue)
{
xml = xml + subStr(tmpStr, quotePos, pos - quotePos);
invalue = false;
}
pos +=1;
if (!inquote)
{
tagStr = 'element';
xml = xml + "<" + tagStr + ">";
convStr = subStr(tmpStr,pos,strLen(tmpStr)-(pos-1));
[retval,expand] = this.convert(tagStr,convStr);
pos += retval;
if (expand)
{
pos -=1;
tmpStr = strPoke(tmpStr,"[",pos);
}
}
break;
//end array, and pop json element
case ']':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos, pos - quotePos);
invalue = false;
}
pos +=1;
if (!inquote)
{
xml = xml + "</element>";
}
return [pos, false];
//begin string type
case '"':
pos +=1;
if (inquote)
{
inquote = false;
//add value without quotation marks if they exist
xml = xml + strReplace(subStr(tmpStr,quotePos,pos-quotePos),'"','');
quotePos=0;
}else
{
inquote = true;
quotePos = pos-1;
}
break;
//parse the value of the element,value pair
case ':':
pos +=1;
if (inquote || invalue)
{
break;
}
//we might have a value other than a string, we will capture all the characters
//and store it in the xml string as a string without quotes.
if (subStr(tmpStr,pos,1) !='"')
{
quotePos = pos;
invalue = true;
}
break;
default:
pos +=1;
break;
}
}
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos -quotePos);
}
xml = xml + "</" + _tag + ">";
return [pos-1,false];
}
view raw gistfile1.cs hosted with ❤ by GitHub

getTag
//get tag from JSON string
container getTag(str _tagStr)
{
str tmpStr = subStr(_tagStr,1,strScan(_tagStr,":",1,strLen(_tagStr))-1);
str retStr = strReplace(tmpStr,'"','');
//XML does not allow the first characer of the tag name to be anything other than a alphabetic character
if (!strFind(retStr,"ABCDEFGHIJKLMNOPQRSTUVWXYZ",1,1))
{
return ["str_" + retStr,retStr];
}
return [retStr,retStr];
}
view raw gistfile1.cs hosted with ❤ by GitHub




Sample JSON exchange rate data from Fixer.io.



XML version after running it through the converter.


Sunday, March 29, 2015

Zebra Printer Label Printing with Dynamics AX 2012 R3


Microsoft Dynamics AX 2012 R3 introduces label printing capabilities.  It does this by sending ZPL commands directly to a Zebra printer.

After researching the new capabilities, in tangent with the new warehouse License Plate functionality I was able to derive enough information to design my own label printing functionality.

There are several stages involved in the development process, which is a prerequisite to printing labels directly to Zebra printers.

  • A label designer, which will print ZPLII code and allow for field names where data substitution needs to occur (optional).
  • A table in Dynamics AX to store the design in ZPL format (memo field).
  • A class that does the data substitution against the ZPL code, which then sends the resulting string of data directly to the printer.
  • A printer capable of interpreting ZPL commands.

This article will explain the prerequisites, and give code examples on how to send label output directly to a Zebra printer.

Note:  I do not have a Zebra printer, and even though the printer I do use, doesn't print anything it does recognize that commands are being sent to the printer.  So I am concluding that should I have a Zebra prnter, the code example in this article would work.  If anyone can confirm that would be great.

For examples in this article, I am going to use the ZebraDesigner version 2.5 from Zebra Technologies Corporation



The first step in the process is to create a label design.  Preferably a design that can be used as a template for reuse.  The example I am creating will contain just one barcode.  I won't go into how to create the label, that can be determined by reading the documentation for ZebraDesigner.



The next step is to print the label to a .prn file.  Notice the highlighted fieldname in the text below.  I came up with that field name.  Later in the process that field name will get substituted with data.


Now that we have a 'template', we need to create the infrastructure in Dynamics AX to store the template and act upon it.  To do this a table was created called ZPLDocumentLayout.  It will store three things.

  • A layout Id.
  • A description of the layout.
  • The ZPL code from the template. (This is a memo field in the table)
To support data entry for the table a form called ZPLDocumentLayout was created and data was populated as follows:


Notice the form has a LayoutId of 'ZPL1'.  That is a code that I made up, and will use later in the process to identify which ZPL template data to use for the labels.  Also notice the template data has been inserted into the record. 

Next I created a table called ZPLDocumentLabel to hold the data for the labels.  It is a simple table with one field called BarcodeId, and holds 3 records, whose values are 'Label1', 'Label2', 'Label3'.

Now that the underlying data, and templates exist, a class needs created specifically to do the data substitution for the template.

Create a class called ZPLDocumentPrint with the following methods.



The code for the methods are listed below:

classDeclaration
class ZPLDocumentPrint
{
RecordSortedList list;
}
view raw gistfile1.cs hosted with ❤ by GitHub
getFieldList
public RecordSortedList getFieldList()
{
return list;
}
view raw gistfile1.cs hosted with ❤ by GitHub
initMenuFields
RecordSortedList initMenuFields()
{
TmpSysTableField tmpField;
DictField dictField;
DictTable dictTable = new DictTable(tableNum(ZPLDocumentLabel));
int length = dictTable.fieldCnt();
int i;
for (i = 1; i <= length; ++i)
{
dictField = new DictField(tableNum(ZPLDocumentLabel), dictTable.fieldCnt2Id(i));
if (!dictField.isSystem() || !dictField.visible())
{
tmpField.FieldId = dictField.id();
tmpField.FieldName = dictField.name();
tmpField.FieldLabel = dictField.label();
list.ins(tmpField);
}
}
return list;
}
view raw gistfile1.cs hosted with ❤ by GitHub
new
public void new()
{
list = new RecordSortedList(tableNum(TmpSysTableField));
list.sortOrder(fieldNum(TmpSysTableField, FieldLabel));
this.initMenuFields();
}
view raw gistfile1.cs hosted with ❤ by GitHub
printDocument

The magic in this whole process is the new CSharp project WHS.DeviceCom, which was introduced in R3.  It allows communicating directly with printers with raw data.  Microsoft.Dynamics.AX.WHS.DeviceCom.Printer::SendStringToPrinter().

public boolean printDocument(PrinterName _printerName,String15 _layoutId,ZPLDocumentLabel _label)
{
str finalStr;
boolean ret;
finalStr = this.translate(zplDocumentLayout::find(_layoutId).zpl, _label);
Microsoft.Dynamics.AX.WHS.DeviceCom.Printer::SendStringToPrinter(_printerName, finalStr);
return ret;
}
view raw gistfile1.cs hosted with ❤ by GitHub
translate

What's fascinating about the translate method it substitutes the field names in the ZPL code, with the table field values in the ZPLDocumentLabel table by matching the field names between the ZPL code and table.

str translate(str _inputStr, ZPLDocumentLabel _label)
{
TmpSysTableField tmpField;
FieldLabel label;
str outputStr;
outputStr = _inputStr;
list.first(tmpField);
while (tmpField.FieldLabel != '')
{
outputStr = strReplace(outputStr, strFmt('$%1$', tmpField.FieldName), _label.(tmpField.FieldId));
label = tmpField.FieldLabel;
tmpField.clear();
list.next(tmpField);
if (label == tmpField.FieldLabel)
{
break;
}
}
return outputStr;
}
view raw gistfile1.txt hosted with ❤ by GitHub

The final piece to the process is to bring it altogether and print the label data from the ZPLDocumentLabel table.  I did this using a job, which invokes the ZPLDocumentPrint class.  The code for the job is as follows:
static void GBS_JEH_ZPLPrintLabel(Args _args)
{
ZPLDocumentPrint zplPrint = new ZPLDocumentPrint();
ZPLDocumentLabel zplLabel;
PrinterName printerName = 'Brother MFC-J470DW Printer';
;
zplPrint.initMenuFields();
while select zplLabel
{
zplPrint.printDocument(printerName,"ZPL1",zplLabel);
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

The output can be captured through a debugging process and entered into a form online at Labelary Online ZPL Viewer

The output from this article looks like this:


Please note that you might need to enable pass through mode on the printer properties,  Prior to WHS.DeviceCom, the method I would use would be to send the ZPL string, wrapped in pass through codes ${ and }$ respectively.

Determining layers in AOT elements for Dynamics AX 2012


When working with new clients its a good to get an idea to the level of customizations they have.  Usually upgrading Dynamics AX for a client it exponentionally proportional in time and effort to the degree of customization.

The code segment shown below, gives an example of how to determine the degree of customization.  The code will transverse the AOT , node by node based on a base node and report back the number of customizations per element.  

The code is fairly useful in that it shows how to transverse the nodes in the AOT, and how to get information about those nodes.

I received an out of memory error when trying to check the layers of multiple base nodes, so I just run it for one base node at a time.  If anyone knows why it would run out of memory, please let me know.


static void GBS_jhodge_checklayer(Args _args)
{
TreeNode TreeNode;
TreeNode BaseTreeNode;
int mask;
counter cnt,tot,isv,var,cus,usr;
#AOT
void checkLayers(str _path)
{
BaseTreeNode = TreeNode::findNode(_path);
TreeNode = BaseTreeNode.AOTfirstChild();
cnt= 0;tot=0 ;isv= 0;var= 0; cus= 0;usr=0 ;
while(TreeNode)
{
tot++;
mask=treeNode.applObjectLayerMask();
if(mask & (1<<UtilEntryLevel::isv)) isv++;
if(mask & (1<<UtilEntryLevel::isp)) isv++;
if(mask & (1<<UtilEntryLevel::var)) var++;
if(mask & (1<<UtilEntryLevel::vap)) var++;
if(mask & (1<<UtilEntryLevel::cus)) cus++;
if(mask & (1<<UtilEntryLevel::cup)) cus++;
if(mask & (1<<UtilEntryLevel::usr)) usr++;
if(mask & (1<<UtilEntryLevel::usp)) usr++;
TreeNode=TreeNode.AOTnextSibling();
}
info( strFmt("%1 Objects ISV(%2) VAR(%3) CUS(%4) USR(%5) %6" ,tot,isv,var,cus,usr,_path));
}
//checkLayers(#TablesPath);
checkLayers(#ClassesPath);
//checkLayers(#FormsPath);
//checkLayers( '\\SSRS Reports\\Reports');
//checkLayers( '\\Visual Studio Projects\\C Sharp Projects' );
//checkLayers( '\\Visual Studio Projects\\Dynamics AX Model Projects' );
}
view raw gistfile1.cs hosted with ❤ by GitHub



Number of elements and number of which are customized layers

Pulling source code from the AOT in Dynamics AX2012


There may come a time when you want to pull the source code to a class method out of the AOT by using XPP.  This can be accomplished by pulling the AOT source code out of the tree node of an object in the AOT as follows:

static void GBS_JEH_ShowCode(Args _args)
{
 
    TreeNode tn;
    str code;
    ;
    tn=TreeNode::findNode(@'Classes\ExchangeRateProviderFixer\classdeclaration');
    code = tn.AOTgetSource();
    info(code);

}

Of course for the code to work, you would have to have an ExchangeRateProviderFixer class in this scenario, so choose your own class and substitute accordingly.


Result of job execution


It would be interesting to iterate through the AOT , pulling out all the source code in the nodes to see what we come up with.


Executing a Stored Procedure from AX


I am not really a fan of having external code supporting functionality within Dynamics AX, but there may come a time when for whatever reason you need to create a stored procedure and have it executed from XPP code.

server static void executeSP()
{
    Connection con = new Connection();
    Statement stmt = con.createStatement();
    ResultSet r;
    str sql;
    SqlStatementExecutePermission perm;
    ;
    sql = strfmt('EXEC [insertdaxjob]');
    perm = new SqlStatementExecutePermission(sql);
    perm.assert();
    try
    {
        stmt.executeUpdate(sql);
    }
    catch (exception::Error)
    {
        print "An error occured in the query.";
        pause;
    }
    CodeAccessPermission::revertAssert();
}

Debugging a Dynamics AX2012 SSRS RDP Report


I haven't put my finger on it yet, but there are times when I am unable to debug an SSRS RDP Report.  I add my breakpoint but during code execution, the system just won't break when it should.  I believe this is related to how the classes are extended.

While faced with this delima, I did some research on the internet and found an article which shows how to create a job for debugging purposes.  Please follow this link for the original reference: Test Report Data Provider in AX2012.

The following is a simple job to invoke an SSRS RDP report.  It doesn't actually print the report, but it does do the meat of the processing so you can debug your RDP code.  Substitute your RDP class and Contract information as required.

static void Job3(Args _args){
TmpABC tempTable;
InventABCDP dataProvider = new InventABCDP();
InventABCContract contract = new InventABCContract();
contract.parmABCModel(ABCModel::Link);
contract.parmCategoryA(10);
contract.parmCategoryC(20);
contract.parmCategoryB(70);
contract.parmInterest(2.5);

dataProvider.parmDataContract(contract);
dataProvider.processReport();
tempTable = dataProvider.getTmpABC();

while
 select tempTable
{
info(tempTable.ItemName);
}
}

Saturday, March 28, 2015

Creating Exchange Rate Providers in Microsoft Dynamics AX 2012 for JSON Rates



Microsoft Dynamics AX 2012 has the frame work for integrating currency exchange rates from exchange rate providers. Please reference Creating Exchange Rate Providers for Microsoft Dynamics AX 2012. The referenced document from Microsoft shows how to customize the frame work to work with Oanda Exchange Rates .

There are other providers, some which offer a fee based API and others which offer a free API. Doing research on these providers led me to JSON Rates. They offer a free API, which has the functionality needed for basic currencyPair conversion rate retrieval.

This blog entry uses the referenced document from Microsoft as a guide to create a provider class in Dynamics AX specifically for use with the JSON Rates API.

The provider class we will create will support the JSON Rates API.

Create the class as ExchangeRateProviderJSONRates and have it extend the ExchangeRateProvider as shown in the class declaration below.

Note: The ExchangeRateProviderIdAttribute is a unique ID that you will need to supply. You can obtain a unique ID from http://createguid.com

[ExchangeRateProviderAttribute,
ExchangeRateProviderIdAttribute('DC8FC177-B377-448E-A18D-FE6B13F94DD9')]
class ExchangeRateProviderJSONRates extends ExchangeRateProvider
{
str serviceUrl;
str serviceUrlForDateRange;
str serviceClient;
int serviceTimeout;
List rates;
List dates;
//ApiKey for account identification when sending requests to JSONRates
#define.APIKey("jr-cd4e735cf7cd8c8b07d7a8fb1491b8ca")
#define.selectNodes("//root/rates/str_")
#define.ServiceTimeout("serviceTimeout")
#define.ServiceURL("ServiceUrl")
#define.OPENEXCHRATESDateFormat("yyyy-MM-dd")
}
view raw gistfile1.cs hosted with ❤ by GitHub

Now create a method called getConfigurationDefaults. This method will provide the default URL used to make requests to the provider.

public ExchangeRateProviderConfigDefaults getConfigurationDefaults()
{
ExchangeRateProviderConfigDefaults configurationDefaults = ExchangeRateProviderConfigDefaults::construct();
configurationDefaults.addNameValueConfigurationPair(#ServiceTimeout, '5000');
configurationDefaults.addNameValueConfigurationPair(#ServiceURL, 'http://jsonrates.com/historical/?from=%1&to=%2&date=%3&apiKey=%4');
return configurationDefaults;
}
view raw gistfile1.cs hosted with ❤ by GitHub

Next create the getExchangeRates method. This method is responsible for constructing the URL to request the currency information from the provider, and store the results in the currency exchange rate tables of Dynamics AX.

public ExchangeRateResponse getExchangeRates(ExchangeRateRequest _exchangeRateRequest)
{
ExchangeRateResponse response = ExchangeRateResponse::construct();
ExchangeRateResponseCurrencyPair currencyPairResponse;
ExchangeRateResponseExchangeRate exchangeRateResponse;
ExchangeRateRequestCurrencyPair currencyPairRequest;
ExchangeRateProviderConfig config = ExchangeRateProviderConfig::construct();
RetailCommonWebAPI webApi;
RetailWebRequest webRequest;
RetailWebResponse webResponse;
str jsonRequestString;
date currentDate;
CurrencyExchangeRate exchangeRate;
ListEnumerator rateEnumerator, dateEnumerator;
System.DateTime fromDate, fromUTCDate;
System.TimeZone localTimeZone;
int compareResult;
str JSONOut;
str dateForRequest;
rates = new List(Types::Real);
dates = new List(Types::Date);
localTimeZone = System.TimeZone::get_CurrentTimeZone();
// Iterate over the requested currency pairs. This is only required for providers
// that support specific currency pairs.
_exchangeRateRequest.initializeCurrencyPairEnumerator();
while(_exchangeRateRequest.moveNextCurrencyPair())
{
serviceTimeout = str2int(config.getPropertyValue(this.getProviderId(), #ServiceTimeout));
serviceUrl = config.getPropertyValue(this.getProviderId(), #ServiceURL);
// Process each date separately.
fromDate = _exchangeRateRequest.parmFromDate();
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
while (compareResult <= 0)
{
currencyPairRequest = _exchangeRateRequest.getCurrentCurrencyPair();
currencyPairResponse = ExchangeRateResponseCurrencyPair::construct();
currencyPairResponse.parmFromCurrency(currencyPairRequest.parmFromCurrency());
currencyPairResponse.parmToCurrency(currencyPairRequest.parmToCurrency());
// All rates are requested with a display factor of 1 for this provider. If the
// rates internally are represented using a different exchange rate display
// factor, the framework will make the necessary adjustments when saving the
// exchange rates.
currencyPairResponse.parmExchangeRateDisplayFactor(ExchangeRateDisplayFactor::One);
// convert to UTC which is required by JSONRATES
fromUTCDate = localTimeZone.ToUniversalTime(fromDate);
dateForRequest = fromUTCDate.ToString(#OPENEXCHRATESDateFormat);
// Build the request URL.
jsonRequestString = strFmt(serviceUrl,
currencyPairRequest.parmFromCurrency(),
currencyPairRequest.parmToCurrency(),
dateForRequest,
#APIKey);
// Configure the request for JSONRATES.
webApi = RetailCommonWebAPI::construct();
webRequest = RetailWebRequest::newUrl(jsonRequestString);
try
{
// Invoke the service
webResponse = webApi.getResponse(webRequest);
JSONOut = webResponse.parmData();
// Parse the XML to retrieve the rate and date.
this.readRate(JSONOut,currencyPairRequest,dateForRequest,DateTimeUtil::date(Global::CLRSystemDateTime2UtcDateTime(fromDate)));
rateEnumerator = rates.getEnumerator();
rateEnumerator.moveNext();
dateEnumerator = dates.getEnumerator();
// Create the Exchange Rate Provider Response.
dateEnumerator.moveNext();
exchangeRate = rateEnumerator.current();
currentDate = dateEnumerator.current();
if (currentDate != dateNull() && exchangeRate)
{
exchangeRateResponse = ExchangeRateResponseExchangeRate::construct();
exchangeRateResponse.parmValidFrom(currentDate);
exchangeRateResponse.parmExchangeRate(exchangeRate);
currencyPairResponse.addExchangeRate(exchangeRateResponse);
currentDate = dateNull();
exchangeRate = 0;
}
}
catch (Exception::CLRError)
{
// The service call did not complete. Swallow the exception and try the next
// currency pair. The framework will be able to determine which currency
// pairs were successfully retrieved and will display the appropriate
// warnings to the user.
}
response.addOrUpdateCurrencyPair(currencyPairResponse);
rates = new List(Types::Real);
dates = new List(Types::Date);
fromDate = fromDate.AddDays(1);
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
}
}
return response;
}
view raw gistfile1.cs hosted with ❤ by GitHub


Next create the getName method. This method is used by Dynamics AX to populate the list of providers that Dynamics AX has been modified to support.

public ExchangeRateProviderName getName()
{
return "JSON Rates";
}
view raw gistfile1.cs hosted with ❤ by GitHub

Once the getName method is created, create the getProviderId method. This method simply returns the unique GUID that was assigned in the class declaration.

public ExchangeRateProviderId getProviderId()
{
return 'DC8FC177-B377-448E-A18D-FE6B13F94DD9';
}
view raw gistfile1.cs hosted with ❤ by GitHub
Next create the getSupportedOptions method.

public ExchangeRateProviderSupportedOptions getSupportedOptions()
{
ExchangeRateProviderSupportedOptions supportedOptions = ExchangeRateProviderSupportedOptions::construct();
supportedOptions.parmDoesSupportSpecificCurrencyPairs(true);
supportedOptions.parmDoesSupportSpecificDates(false);
supportedOptions.parmFixedBaseIsoCurrency('');
return supportedOptions;
}
view raw gistfile1.cs hosted with ❤ by GitHub

The last method that needs created is the readRate method. It is responsible for parsing the XML response string from the provider, and storing the exchange rate and currency information in the Lists.

private void readRate(str _jsonString,ExchangeRateRequestCurrencyPair _currencyPairRequest,str _currentDate,date _listDate)
{
XmlDocument xmlDocument;
XmlNode node;
date currentDate;
JSON2Xml json2xml = new JSON2Xml();
str xml = json2xml.convert2xml(_jsonString);
str singleNodeStr = strFmt("%1%2/rate",#selectNodes,_currentDate);
;
xmlDocument = new xmlDocument();
xmlDocument.loadXml(xml);
node = xmlDocument.selectSingleNode(singleNodeStr);
rates.addEnd(any2real(node.text()));
currentDate = str2Date(_currentDate,312);
dates.addEnd(_listDate);
}
view raw gistfile1.cs hosted with ❤ by GitHub


A JSON to XML converter class will also need to be added as a dependency, which is coded as follows:

classdeclaration
class JSON2XML
{
BinData binData;
}
view raw gistfile1.cs hosted with ❤ by GitHub


loadFromFile
//load the JSON data from a file
public str loadFromFile(FilenameOpen _file)
{
FileIOPermission _perm = new FileIOPermission(_file,"r");
binData = new binData();
_perm.assert();
binData.loadFile(_file);
return binData.getStrData();
}
view raw gistfile1.cs hosted with ❤ by GitHub

convert2xml
//rudimentry method for converting JSON to XML
public str convert2xml(str _json)
{
#define.SPACE(32)
//remove whitespace from JSON string
str stripSpace(str _str)
{
int i;
str tmpStr;
boolean inquote;
for(i=1;i<=strLen(_str);i++)
{
if (subStr(_str,i,1) == '"')
{
inquote=true;
}else
{
inquote = false;
}
if (char2num(_str,i) <= #SPACE && !inquote)
{
continue;
}
tmpStr = tmpStr + subStr(_str,i,1);
}
return tmpStr;
}
//add header to xml
xml = '<?xml version="1.0" encoding="UTF-8"?>';
xml = xml + "<root>";
//convert
this.convert("root",stripSpace(_json));
return xml;
}
view raw gistfile1.cs hosted with ❤ by GitHub
convert
//recursive method to parse JSON string and construct XML.
container convert(str _tag,str _str)
{
int i = strLen(_str);
str tmpStr = _str;
str tagStr,origTagStr;
str convStr;
int pos;
int quotePos;
int retval;
boolean expand;
boolean inquote;
boolean invalue;
pos = 1;
while (pos<=i)
{
switch(subStr(tmpStr,pos,1))
{
//assign end tag and pop if not in quotes
case ',':
if (!inquote)
{
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos- quotePos);
invalue = false;
}
xml = xml + "</" + _tag + ">";
return [pos,true];
}
pos +=1;
break;
//beginning of a tag if not in quotes, push json element, value
case '{':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos - quotePos);
inValue = false;
}
pos +=1;
if (!inquote)
{
[tagStr,origTagStr] = this.getTag(subStr(tmpStr,pos,strLen(tmpStr)-pos));
xml = xml + "<" + tagStr + ">";
pos += strLen(origTagStr) + 2;
convStr = subStr(tmpStr,pos,strLen(tmpStr)-(pos-1));
[retval,expand] = this.convert(tagStr,convStr);
pos += retval;
//if we are going to expand, then poke a '{' to force another nesting tag
if (expand)
{
pos -=1;
tmpStr = strPoke(tmpStr,"{",pos);
}
}
break;
//ending of the tag if not in quotes, pop json element
case '}':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos -quotePos);
invalue = false;
}
//pos +=1;
if (!inquote)
{
xml = xml + "</" + _tag + ">";
}
return [pos,false];
//begin array if not in quote and push json element, value pairs
case '[':
if (invalue)
{
xml = xml + subStr(tmpStr, quotePos, pos - quotePos);
invalue = false;
}
pos +=1;
if (!inquote)
{
tagStr = 'element';
xml = xml + "<" + tagStr + ">";
convStr = subStr(tmpStr,pos,strLen(tmpStr)-(pos-1));
[retval,expand] = this.convert(tagStr,convStr);
pos += retval;
if (expand)
{
pos -=1;
tmpStr = strPoke(tmpStr,"[",pos);
}
}
break;
//end array, and pop json element
case ']':
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos, pos - quotePos);
invalue = false;
}
pos +=1;
if (!inquote)
{
xml = xml + "</element>";
}
return [pos, false];
//begin string type
case '"':
pos +=1;
if (inquote)
{
inquote = false;
//add value without quotation marks if they exist
xml = xml + strReplace(subStr(tmpStr,quotePos,pos-quotePos),'"','');
quotePos=0;
}else
{
inquote = true;
quotePos = pos-1;
}
break;
//parse the value of the element,value pair
case ':':
pos +=1;
if (inquote || invalue)
{
break;
}
//we might have a value other than a string, we will capture all the characters
//and store it in the xml string as a string without quotes.
if (subStr(tmpStr,pos,1) !='"')
{
quotePos = pos;
invalue = true;
}
break;
default:
pos +=1;
break;
}
}
if (invalue)
{
xml = xml + subStr(tmpStr,quotePos,pos -quotePos);
}
xml = xml + "</" + _tag + ">";
return [pos-1,false];
}
view raw gistfile1.cs hosted with ❤ by GitHub
getTag
//get tag from JSON string
container getTag(str _tagStr)
{
str tmpStr = subStr(_tagStr,1,strScan(_tagStr,":",1,strLen(_tagStr))-1);
str retStr = strReplace(tmpStr,'"','');
//XML does not allow the first characer of the tag name to be anything other than a alphabetic character
if (!strFind(retStr,"ABCDEFGHIJKLMNOPQRSTUVWXYZ",1,1))
{
return ["str_" + retStr,retStr];
}
return [retStr,retStr];
}
view raw gistfile1.cs hosted with ❤ by GitHub


Now that the class has been created, it can be used in currency configuration and setup within Dynamics AX.




currency2







Friday, March 27, 2015

Creating Exchange Rate Providers in Microsoft Dynamics AX 2012 for Xignite



Microsoft Dynamics AX 2012 has the frame work for integrating currency exchange rates from exchange rate providers. Please reference Creating Exchange Rate Providers for Microsoft Dynamics AX 2012. The referenced document from Microsoft shows how to customize the frame work to work with Oanda Exchange Rates .

There are other providers, some which offer a fee based API and others which offer a free API. Doing research on these providers led me to Xignite. They offer a fee based API, which is fairly competitive with other providers.

This blog entry uses the referenced document from Microsoft as a guide to create a provider class in Dynamics AX specifically for use with Xignite API.

The provider class we will create will be the fee based provider. The fee based provider allows for selection of base currency, and specific currency pairs.

Create the class as ExchangeRateProviderXigniteRates and have it extend the ExchangeRateProvider as shown in the class declaration below.

Note: The ExchangeRateProviderIdAttribute is a unique ID that you will need to supply. You can obtain a unique ID from http://createguid.com

[ExchangeRateProviderAttribute,
ExchangeRateProviderIdAttribute('70441D3B-E1BC-430E-9192-2FB435BAB82A')]
class ExchangeRateProviderXignite extends ExchangeRateProvider
{
str serviceUrl;
str serviceUrlForDateRange;
str serviceClient;
int serviceTimeout;
List rates;
List dates;
//Token for account identification when sending requests to Xignite
//get your own token at Xignite
#define.Token("get your own token")
#define.HttpWebRequestMethod("GET")
#define.HttpWebRequestContentType("application/xml")
#define.HttpHeaderAuthorization("Authorization")
#define.NameSpaceAlias("ns1")
#define.NameSpaceURL("http://www.xignite.com/services/")
#define.QuoteXPath("//HistoricalRate")
#define.BidXPath("//ns1:Average")
#define.DateXPath("//ns1:EndDate")
#define.ServiceTimeout("serviceTimeout")
#define.ServiceURL("ServiceUrl")
#define.XIGNITEDateFormat("MM/dd/yyyy")
}
view raw gistfile1.cs hosted with ❤ by GitHub

Now create a method called getConfigurationDefaults. This method will provide the default URL used to make requests to the provider.

public ExchangeRateProviderConfigDefaults getConfigurationDefaults()
{
ExchangeRateProviderConfigDefaults configurationDefaults = ExchangeRateProviderConfigDefaults::construct();
configurationDefaults.addNameValueConfigurationPair(#ServiceTimeout, '5000');
//Requires Enterprise account
configurationDefaults.addNameValueConfigurationPair(#ServiceURL, 'http://globalcurrencies.xignite.com/xGlobalCurrencies.xml/GetHistoricalRate?Symbol=%1%2&AsOfDate=%3&FixingTime=01:00&PriceType=Mid&_token=%4');
return configurationDefaults;
}
view raw gistfile1.cs hosted with ❤ by GitHub

Next create the getExchangeRates method. This method is responsible for constructing the URL to request the currency information from the provider, and store the results in the currency exchange rate tables of Dynamics AX.

public ExchangeRateResponse getExchangeRates(ExchangeRateRequest _exchangeRateRequest)
{
ExchangeRateResponse response = ExchangeRateResponse::construct();
ExchangeRateResponseCurrencyPair currencyPairResponse;
ExchangeRateResponseExchangeRate exchangeRateResponse;
ExchangeRateRequestCurrencyPair currencyPairRequest;
ExchangeRateProviderConfig config = ExchangeRateProviderConfig::construct();
RetailCommonWebAPI webApi;
RetailWebRequest webRequest;
RetailWebResponse webResponse;
str xIgniteRequestString;
date currentDate;
CurrencyExchangeRate exchangeRate;
//System.Net.WebResponse webResponse;
System.IO.StreamReader streamReader;
System.IO.Stream stream;
System.Net.HttpWebRequest httpWebRequest;
ListEnumerator rateEnumerator, dateEnumerator;
System.Net.WebHeaderCollection webCollection;
System.DateTime fromDate, fromUTCDate;
System.TimeZone localTimeZone;
int compareResult;
str XMLOut;
str dateForRequest;
rates = new List(Types::Real);
dates = new List(Types::Date);
localTimeZone = System.TimeZone::get_CurrentTimeZone();
// Iterate over the requested currency pairs. This is only required for providers
// that support specific currency pairs.
_exchangeRateRequest.initializeCurrencyPairEnumerator();
while(_exchangeRateRequest.moveNextCurrencyPair())
{
serviceTimeout = str2int(config.getPropertyValue(this.getProviderId(), #ServiceTimeout));
serviceUrl = config.getPropertyValue(this.getProviderId(), #ServiceURL);
// Process each date separately.
fromDate = _exchangeRateRequest.parmFromDate();
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
while (compareResult <= 0)
{
currencyPairRequest = _exchangeRateRequest.getCurrentCurrencyPair();
currencyPairResponse = ExchangeRateResponseCurrencyPair::construct();
currencyPairResponse.parmFromCurrency(currencyPairRequest.parmFromCurrency());
currencyPairResponse.parmToCurrency(currencyPairRequest.parmToCurrency());
// All rates are requested with a display factor of 1 for this provider. If the
// rates internally are represented using a different exchange rate display
// factor, the framework will make the necessary adjustments when saving the
// exchange rates.
currencyPairResponse.parmExchangeRateDisplayFactor(ExchangeRateDisplayFactor::One);
// convert to UTC
fromUTCDate = localTimeZone.ToUniversalTime(fromDate);
dateForRequest = fromUTCDate.ToString(#XIGNITEDateFormat);
// Build the request URL.
xIgniteRequestString = strFmt(serviceUrl,
currencyPairRequest.parmFromCurrency(),
currencyPairRequest.parmToCurrency(),
dateForRequest,
#Token);
// Configure the request for XIGNITE.
webApi = RetailCommonWebAPI::construct();
webRequest = RetailWebRequest::newUrl(xIgniteRequestString);
try
{
// Invoke the service
webResponse = webApi.getResponse(webRequest);
XMLOut = webResponse.parmData();
// Parse the XML to retrieve the rate and date.
this.readRate(XMLOut);
rateEnumerator = rates.getEnumerator();
rateEnumerator.moveNext();
dateEnumerator = dates.getEnumerator();
// Create the Exchange Rate Provider Response.
dateEnumerator.moveNext();
exchangeRate = rateEnumerator.current();
currentDate = dateEnumerator.current();
if (currentDate != dateNull() && exchangeRate)
{
exchangeRateResponse = ExchangeRateResponseExchangeRate::construct();
exchangeRateResponse.parmValidFrom(currentDate);
exchangeRateResponse.parmExchangeRate(exchangeRate);
currencyPairResponse.addExchangeRate(exchangeRateResponse);
currentDate = dateNull();
exchangeRate = 0;
}
}
catch (Exception::CLRError)
{
// The service call did not complete. Swallow the exception and try the next
// currency pair. The framework will be able to determine which currency
// pairs were successfully retrieved and will display the appropriate
// warnings to the user.
}
response.addOrUpdateCurrencyPair(currencyPairResponse);
rates = new List(Types::Real);
dates = new List(Types::Date);
fromDate = fromDate.AddDays(1);
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
}
}
return response;
}
view raw gistfile1.cs hosted with ❤ by GitHub


Next create the getName method. This method is used by Dynamics AX to populate the list of providers that Dynamics AX has been modified to support.

public ExchangeRateProviderName getName()
{
return "Xignite";
}
view raw gistfile1.cs hosted with ❤ by GitHub

Once the getName method is created, create the getProviderId method. This method simply returns the unique GUID that was assigned in the class declaration.

public ExchangeRateProviderId getProviderId()
{
return '70441D3B-E1BC-430E-9192-2FB435BAB82A';
}
view raw gistfile1.cs hosted with ❤ by GitHub
Next create the getSupportedOptions method.

public ExchangeRateProviderSupportedOptions getSupportedOptions()
{
ExchangeRateProviderSupportedOptions supportedOptions = ExchangeRateProviderSupportedOptions::construct();
supportedOptions.parmDoesSupportSpecificCurrencyPairs(true);
supportedOptions.parmDoesSupportSpecificDates(false);
supportedOptions.parmFixedBaseIsoCurrency('');
return supportedOptions;
}
view raw gistfile1.cs hosted with ❤ by GitHub

The last method that needs created is the readRate method. It is responsible for parsing the XML response string from the provider, and storing the exchange rate and currency information in the Lists.

private void readRate(str _xmlString)
{
XmlDocument xmlDom = new XmlDocument();
XmlNode xmlRootNode, xmlBidNode, xmlDateNode;
XmlNamespaceManager nmgr = new XmlNamespaceManager(new XmlNameTable());
CurrencyExchangeRate exchangeRate;
ValidFromDate exchangeDate;
str value;
xmlDom.LoadXml(_xmlString);
nmgr.addNamespace(#NameSpaceAlias,#NameSpaceURL);
xmlRootNode = xmlDom.documentElement();
if (xmlRootNode)
{
// Find the exchange rate
xmlBidNode = xmlRootNode.selectSingleNode(#BidXPath,nmgr);
if (xmlBidNode)
{
value = xmlBidNode.InnerText();
exchangeRate = str2num(value);
if (exchangeRate)
{
rates.addEnd(exchangeRate);
}
}
//Find the date of the exchange rate.
xmlDateNode = xmlRootNode.selectSingleNode(#DateXPath,nmgr);
if (xmlDateNode)
{
value = xmlDateNode.InnerText();
// convert the date from UTC to local timezone.
exchangeDate = str2Date(Value,213);
if (exchangeRate)
{
dates.addEnd(exchangeDate);
}
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

Now that the class has been created, it can be used in currency configuration and setup within Dynamics AX.







currency2







Creating Exchange Rate Providers in Microsoft Dynamics AX 2012 for Open Exchange Rates



Microsoft Dynamics AX 2012 has the frame work for integrating currency exchange rates from exchange rate providers. Please reference Creating Exchange Rate Providers for Microsoft Dynamics AX 2012. The referenced document from Microsoft shows how to customize the frame work to work with Oanda Exchange Rates .

There are other providers, some which offer a fee based API and others which offer a free API. Doing research on these providers led me to Open Exchange Rates. They offer an API at no cost for personal use, and a fee based API for Enterprise users, which is fairly competitive with other providers.

This blog entry uses the referenced document from Microsoft as a guide to create a provider class in Dynamics AX specifically for use with the free version of Open Exchange Rates, and the fee based Open Exchange Rates API.


A challenge with using this provider is that it uses JSON instead of XML.

The provider class we will create will be the fee based provider. The fee based provider allows for selection of base currency, and specific currency pairs.

Create the class as ExchangeRateProviderOpenExchRates and have it extend the ExchangeRateProvider as shown in the class declaration below.

Note: The ExchangeRateProviderIdAttribute is a unique ID that you will need to supply. You can obtain a unique ID from http://createguid.com

[ExchangeRateProviderAttribute,
ExchangeRateProviderIdAttribute('F3D5E522-7EFD-4CBF-895E-6DF6596DF8DF')]
class ExchangeRateProviderOpenExchRates extends ExchangeRateProvider
{
str serviceUrl;
str serviceUrlForDateRange;
str serviceClient;
int serviceTimeout;
List rates;
List dates;
//AppId for account identification when sending requests to Open Exchange Rates
//You will need to get your own Appid from https://openexchangerates.org
#define.AppId("get your own appId")
#define.ServiceTimeout("serviceTimeout")
#define.ServiceURL("ServiceUrl")
#define.OPENEXCHRATESDateFormat("yyyy-MM-dd")
}
view raw gistfile1.cs hosted with ❤ by GitHub

Now create a method called getConfigurationDefaults. This method will provide the default URL used to make requests to the provider.

public ExchangeRateProviderConfigDefaults getConfigurationDefaults()
{
ExchangeRateProviderConfigDefaults configurationDefaults = ExchangeRateProviderConfigDefaults::construct();
configurationDefaults.addNameValueConfigurationPair(#ServiceTimeout, '5000');
//Requires Enterprise account
configurationDefaults.addNameValueConfigurationPair(#ServiceURL, 'http://openexchangerates.org/api/historical/%1.json?app_id=%2&base=%3&symbols=%4');
return configurationDefaults;
}
view raw gistfile1.cs hosted with ❤ by GitHub

Next create the getExchangeRates method. This method is responsible for constructing the URL to request the currency information from the provider, and store the results in the currency exchange rate tables of Dynamics AX.

public ExchangeRateResponse getExchangeRates(ExchangeRateRequest _exchangeRateRequest)
{
ExchangeRateResponse response = ExchangeRateResponse::construct();
ExchangeRateResponseCurrencyPair currencyPairResponse;
ExchangeRateResponseExchangeRate exchangeRateResponse;
ExchangeRateRequestCurrencyPair currencyPairRequest;
ExchangeRateProviderConfig config = ExchangeRateProviderConfig::construct();
RetailCommonWebAPI webApi;
RetailWebRequest webRequest;
RetailWebResponse webResponse;
str openExchRatesRequestString;
date currentDate;
CurrencyExchangeRate exchangeRate;
ListEnumerator rateEnumerator, dateEnumerator;
System.DateTime fromDate, fromUTCDate;
System.TimeZone localTimeZone;
int compareResult;
str JSONOut;
str dateForRequest;
rates = new List(Types::Real);
dates = new List(Types::Date);
localTimeZone = System.TimeZone::get_CurrentTimeZone();
// Iterate over the requested currency pairs. This is only required for providers
// that support specific currency pairs.
_exchangeRateRequest.initializeCurrencyPairEnumerator();
while(_exchangeRateRequest.moveNextCurrencyPair())
{
serviceTimeout = str2int(config.getPropertyValue(this.getProviderId(), #ServiceTimeout));
serviceUrl = config.getPropertyValue(this.getProviderId(), #ServiceURL);
// Process each date separately.
fromDate = _exchangeRateRequest.parmFromDate();
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
while (compareResult <= 0)
{
currencyPairRequest = _exchangeRateRequest.getCurrentCurrencyPair();
currencyPairResponse = ExchangeRateResponseCurrencyPair::construct();
currencyPairResponse.parmFromCurrency(currencyPairRequest.parmFromCurrency());
currencyPairResponse.parmToCurrency(currencyPairRequest.parmToCurrency());
// All rates are requested with a display factor of 1 for this provider. If the
// rates internally are represented using a different exchange rate display
// factor, the framework will make the necessary adjustments when saving the
// exchange rates.
currencyPairResponse.parmExchangeRateDisplayFactor(ExchangeRateDisplayFactor::One);
// convert to UTC which is required by OPENEXCHANGERATES
fromUTCDate = localTimeZone.ToUniversalTime(fromDate);
dateForRequest = fromUTCDate.ToString(#OPENEXCHRATESDateFormat);
// Build the request URL.
openExchRatesRequestString = strFmt(serviceUrl,
dateForRequest,
#AppId,
currencyPairRequest.parmFromCurrency(),
currencyPairRequest.parmToCurrency());
// Configure the request for OPENEXCHRATES.
webApi = RetailCommonWebAPI::construct();
webRequest = RetailWebRequest::newUrl(openExchRatesRequestString);
try
{
// Invoke the service
webResponse = webApi.getResponse(webRequest);
JSONOut = webResponse.parmData();
// Parse the JSON to retrieve the rate and date.
this.readRate(JSONOut,currencyPairRequest,DateTimeUtil::date(Global::CLRSystemDateTime2UtcDateTime(fromDate)));
rateEnumerator = rates.getEnumerator();
rateEnumerator.moveNext();
dateEnumerator = dates.getEnumerator();
// Create the Exchange Rate Provider Response.
dateEnumerator.moveNext();
exchangeRate = rateEnumerator.current();
currentDate = dateEnumerator.current();
if (currentDate != dateNull() && exchangeRate)
{
exchangeRateResponse = ExchangeRateResponseExchangeRate::construct();
exchangeRateResponse.parmValidFrom(currentDate);
exchangeRateResponse.parmExchangeRate(exchangeRate);
currencyPairResponse.addExchangeRate(exchangeRateResponse);
currentDate = dateNull();
exchangeRate = 0;
}
}
catch (Exception::CLRError)
{
// The service call did not complete. Swallow the exception and try the next
// currency pair. The framework will be able to determine which currency
// pairs were successfully retrieved and will display the appropriate
// warnings to the user.
}
response.addOrUpdateCurrencyPair(currencyPairResponse);
rates = new List(Types::Real);
dates = new List(Types::Date);
fromDate = fromDate.AddDays(1);
compareResult = fromDate.CompareTo(_exchangeRateRequest.parmToDate());
}
}
return response;
}
view raw gistfile1.cs hosted with ❤ by GitHub


Next create the getName method. This method is used by Dynamics AX to populate the list of providers that Dynamics AX has been modified to support.

public ExchangeRateProviderName getName()
{
return "Open Exchange Rates";
}
view raw gistfile1.cs hosted with ❤ by GitHub

Once the getName method is created, create the getProviderId method. This method simply returns the unique GUID that was assigned in the class declaration.

public ExchangeRateProviderId getProviderId()
{
return 'F3D5E522-7EFD-4CBF-895E-6DF6596DF8DF';
}
view raw gistfile1.cs hosted with ❤ by GitHub
Next create the getSupportedOptions method.

public ExchangeRateProviderSupportedOptions getSupportedOptions()
{
ExchangeRateProviderSupportedOptions supportedOptions = ExchangeRateProviderSupportedOptions::construct();
supportedOptions.parmDoesSupportSpecificCurrencyPairs(true);
supportedOptions.parmDoesSupportSpecificDates(false);
supportedOptions.parmFixedBaseIsoCurrency('');
return supportedOptions;
}
view raw gistfile1.cs hosted with ❤ by GitHub

The last method that needs created is the readRate method. It is responsible for parsing the JSON response string from the provider, and storing the exchange rate and currency information in the Lists.

private void readRate(str _jsonString,ExchangeRateRequestCurrencyPair _currencyPairRequest,date _currentDate)
{
Map ratesMap = new Map(Types::String,Types::Real);
MapEnumerator me;
//I have not found a good way to deserialize JSON, so I have written custom code to parse the JSON string
void getRates()
{
int pos = strScan(_jsonString,'"rates": {' ,0,strlen(_jsonString))+10; // determine position of currency rates in JSON string
str temp = subStr(_jsonString,pos,strLen(_jsonString)-pos); // get currency rates into temp string
List listRates = strSplit(temp,","); //split the currency rate string into a list
List listCountry; //list used to temporarily store country specific currency and rates
ListEnumerator lir,lic; //enumerators for rates and country specific rates
str strRate;
str strCountry;
str strCountryRate;
lir = listRates.getEnumerator();
//look through all rates
while (lir.moveNext())
{
//strRate includes both the ToCurrency Code and the Rate
strRate = lir.current();
//split the country specific rate into it's own list so we can add it to the ratesMap
listCountry = strSplit(strRate,":");
lic = listCountry.getEnumerator();
//get the 'TO' country specific currency
if (lic.moveNext())
{
strCountry = lic.current();
//get the 'TO' country specific rate
if (lic.moveNext())
{
strCountryRate = lic.current();
strCountry = strReplace(strCountry,'"','');
//build map of 'TO' country specific currencies and rates
ratesMap.insert(subStr(strCountry,strLen(strCountry)-2,3),any2real(strCountryRate));
}
}
}
}
//Parse JSON string into ratesMAP
getRates();
if (ratesMap.exists(_currencyPairRequest.parmToCurrency()))
{
rates.addEnd(ratesMap.lookup(_currencyPairRequest.parmToCurrency()));
dates.addEnd(_currentDate);
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

The version of the class that supports the free API is basically the same except for the URL request string. The free version only supports 2 options. The APPId, and the date parameter. The result is a currency list for all supported currencies but it is USD based. However the class filters to just the currency conversion needed by date. To modify the class to use the free version, simply change the getConfigurationDefaults method as follows:

public ExchangeRateProviderConfigDefaults getConfigurationDefaults()
{
ExchangeRateProviderConfigDefaults configurationDefaults = ExchangeRateProviderConfigDefaults::construct();
configurationDefaults.addNameValueConfigurationPair(#ServiceTimeout, '5000');
//using free account
configurationDefaults.addNameValueConfigurationPair(#ServiceURL, 'http://openexchangerates.org/api/historical/%1.json?app_id=%2');
return configurationDefaults;
}
view raw gistfile1.cs hosted with ❤ by GitHub

Now that the class has been created, it can be used in currency configuration and setup within Dynamics AX.




currency1


currency2