Tuesday, November 11, 2014

How to convert a wrong date

Standard str2date function tries to fix a wrong date. For example, if we got as a parameter fantastic June 31th, it returns us June 30, which can be a bad result for your business case.



There are at least two possible solutions for that.

First, by converting the result back to a string and comparing it with the initial string.

private container cgiValidateDate(str _dateStr)
{
    date                retDate     = str2Date(_dateStr, #cgiDateFormat);
    boolean             isOK        = true;
    str                 madeDateStr = date2StrUsr(retDate, DateFlags::FormatAll);

    if(retDate == dateNull() || _dateStr != madeDateStr)
    {
        error(strFmt("Date %1 is incorrect", _dateStr));
        isOK = false;
    }

    return [isOK, retDate];
}


Second, by using .Net function tryParse.


private container cgiValidateDate(str _dateStr)
{
    date                retDate;
    utcDateTime         retDateTime;
    boolean             isOK        = true;
    
    if(!System.DateTime::TryParse(_dateStr, byref retDateTime))
    {
        error(strFmt("Date %1 is incorrect", _dateStr));
        isOK = false;
    }
    else
    {
        retDate = DateTimeUtil::date(retDateTime);
    }
    return [isOK, retDate];
}


Happy date converting!

Wednesday, November 5, 2014

Dialog field with multiple choice

This is a small tutorial class on how to work with the new AX 2012 SysLookupMultiSelectCtrl class.

There are good examples on the internet as well as a tutorial class in AX 2012 itself but I want to explain how to pack/unpack values for this field and make it mandatory.

Let's say we need to select multiple sites in the dialog.



I hope my comments inline will be enough. If not let me know, please.


class tmxMultiSelectSiteTutorial extends RunBase
{
    FormBuildStringControl      fbscMultiSite;
    FormStringControl           fscMultiSite;
    container                   siteIds;
    str                         siteIdsStr;

    SysLookupMultiSelectCtrl    multiSiteCtrl;
    DialogRunbase               dialog;

    #define.CurrentVersion(1)
    #define.Version1(1)
    #localmacro.CurrentList
        siteIdsStr
    #endmacro
}



protected Object dialog()
{
    dialog = super();
    // add a new form build control for multiple choice; string type
    fbscMultiSite        = dialog.curFormGroup().addControl(FormControlType::String, identifierstr(AnyFormControlNameYouLike));
    fbscMultiSite.label('Site');

    return dialog;
}





public void dialogPostRun(DialogRunbase _dialog)
{
    FormRun formRun;

    super(dialog);

    formRun = _dialog.dialogForm().formRun();

    if (formRun)
    {
        // to get the access to the form control we created on the dialog
        fscMultiSite = formRun.design().control(fbscMultiSite.id());
        // create multiple loookup of SysLookupMultiSelectCtrl type
        // cgiInventSite query must exist in AOT; simply SiteId and Name from InventSite table
        multiSiteCtrl = SysLookupMultiSelectCtrl::construct(formRun, fscMultiSite, querystr(cgiInventSite));
        // to underline it red; actually it does not validate; so check the Validate method
        multiSiteCtrl.setMandatory(true);
        // if we restored from last values
        if(siteIdsStr)
        {
            //then we convert the string to container
            siteIds = str2con(siteIdsStr);
            // after create the special container of SiteIds and Names
            multiSiteCtrl.set(this.siteIds2Names(siteIds));
        }
    }
}





public boolean getFromDialog()
{
    if (multiSiteCtrl)
    {
        // selected sites convert to container of RecIds
        siteIds     = multiSiteCtrl.get();
        // convert it to string for pack/unpack
        siteIdsStr  = con2Str(siteIds);
    }
    return super();
}





private container siteIds2Names(container _c)
{
    InventSite      inventSite;
    container       contSiteId, contRecId;
    int             i, cLen = conLen(_c);

    for (i = 1 ; i <= cLen ; i++)
    {
        inventSite = inventSite::findRecId(conPeek(_c, i));
        // this part will be visible
        contSiteId += [inventSite.SiteId];
        // this part will be used by SysLookupMultiSelectCtrl as invisible
        contRecId  += [inventSite.RecId];
    }
    return [contRecId, contSiteId];
}





public boolean validate(Object _calledFrom = null)
{
    boolean ret;

    ret = super(_calledFrom);

    if(!conPeek(siteIds, 1))
    {
         ret = checkFailed('Site must be selected!');
    }

    return ret;
}





public void run()
{
    InventSite  inventSite;
    int         i;
    int         conNum = conLen(siteIds);

    // any business logic for the selected sites
    for( i = 1; i<=conNum; i++)
    {
        inventSite = inventSite::findRecId(conPeek(siteIds, i));
        info(strFmt("Site: %1 - %2", inventSite.SiteId, inventSite.Name));
    }
}






Labels and Best Practices




Guys, please, do not forget that AX is really multilingual system and there are many other languages than English. Happy labelling!

Monday, October 27, 2014

Find Price and Not Lose It

A system bug in PriceDisc class that leads to losing the found price/discount. Still presented in AX 2012 R3 (Application version 6.3.164.0) The local subroutine findDisc in findDisc method is called inside the buffer loop and must be fixed as follows.
void findDisc()
    {

        if ((discDate >= localFromDate  || ! localFromDate)
            && (discDate <= localToDate || ! localToDate))
        {
            if (_relation == PriceType::EndDiscPurch ||
                _relation == PriceType::EndDiscSales )
            {
                // for end discounts, the QuantiyAmountField field contains order total amounts, not quantities
                if (this.calcCur2CurPriceAmount(localQuantityAmountFrom, priceDiscTable) <= qty &&
                    ((qty < this.calcCur2CurPriceAmount(localQuantityAmountTo, priceDiscTable)) || !localQuantityAmountTo))
                {
                    reselectBuffer();

                    discExist               = true;
                    discAmount             += this.calcCur2CurPriceAmount(priceDiscTable.Amount, priceDiscTable)/ this.priceUnit();
                    percent1               += priceDiscTable.Percent1;
                    percent2               += priceDiscTable.Percent2;
                      // Begin: Alexey Voytsekhovskiy Not to lose the buffer!
                    actualDiscTable        =  priceDiscTable.data();
                    //actualDiscTable        = priceDiscTable;
                    // End: Alexey Voytsekhovskiy
                      
                  }
            }
            else
            {
                if (localQuantityAmountFrom <= qty
                    && (qty < localQuantityAmountTo || !localQuantityAmountTo))
                {
                    reselectBuffer();

                    discExist               = true;
                    discAmount             += this.calcCur2CurPriceAmount(priceDiscTable.Amount, priceDiscTable)/ this.priceUnit();
                    percent1               += priceDiscTable.Percent1;
                    percent2               += priceDiscTable.Percent2;
                      // Begin: Alexey Voytsekhovskiy Not to lose the buffer!
                    actualDiscTable        =  priceDiscTable.data();
                    //actualDiscTable        = priceDiscTable;
                    // End: Alexey Voytsekhovskiy
                  }
            }
        }
    }

 
The subroutine findPrice in findPriceAgreement method is called inside the buffer loop and must be fixed as follows.
void findPrice()
    {
        if (((discDate >= localFromDate || ! localFromDate)
            &&(discDate <= localToDate  || ! localToDate))
        && (localQuantityAmountFrom <= absQty
            &&(localQuantityAmountTo > absQty || !localQuantityAmountTo)))
        {
            if (cacheMode)
            {
                priceDiscTable = PriceDiscTable::findRecId(localRecid);
            }

            if (this.calcCur2CurPriceAmount(priceDiscTable.calcPriceAmount(absQty),  priceDiscTable) < this.calcPriceAmount(absQty) ||
                ! priceExist)
            {
                priceUnit               = priceDiscTable.priceUnit();
                price                   = this.calcCur2CurPriceAmount(priceDiscTable.price(),  priceDiscTable);

                if (salesParameters.ApplySmartRoundingAfterConversion && (priceDiscTable.Currency != currency) &&
                    relation == PriceType::PriceSales)
                {
                    price = PriceDiscSmartRounding::smartRound(price,Currency::find(currency));
                }

                markup                  = this.calcCur2CurPriceAmount(priceDiscTable.markup(),  priceDiscTable);

                pdsCalculationId        = priceDiscTable.PDSCalculationId;

                if (priceDiscTable.DisregardLeadTime)
                {
                    this.updateLeadTime();
                }
                else
                {
                    deliveryDays        = priceDiscTable.DeliveryTime;
                    calendarDays        = priceDiscTable.CalendarDays;
                }

                // <GEERU>
                inventBaileeFreeDays    = priceDiscTable.InventBaileeFreeDays_RU;
                // </GEERU>
                  // Begin: Alexey Voytsekhovskiy, Not to lose the buffer!
                actualPriceTable        = priceDiscTable.data();
                 // actualPriceTable        = priceDiscTable;
                // End: Alexey Voytsekhovskiy
                  
                  priceExist              = true;
  
                  // <GIN>
                  // Begin: Alexey Voytsekhovskiy, ThinkMax, 04Dec13, UAP_FDD017_PriceSimulation
                uapPriceDiscTableRecId  = priceDiscTable.RecId;
                // End: Alexey Voytsekhovskiy, ThinkMax, 04Dec13, UAP_FDD017_PriceSimulation
                  if (countryRegion_IN)
                {
                    // Firstly, retrieve the MRP from the trade agreement. If there is no MRP defined in
                    // the trade agreement, the MRP should be retrieved from the item master.
                    maxRetailPrice = this.calcCur2CurPriceAmount(
                        priceDiscTable.MaximumRetailPrice_IN ?
                            priceDiscTable.MaximumRetailPrice_IN :
                            InventTableModule::find(itemId, moduleType).maxRetailPrice_IN(),
                        priceDiscTable);
                }
                // </GIN>
            }
        }
    }

 
Happy pricing!

Friday, October 24, 2014

How to iterate project group members: Tables, EDT, etc

Based on S. Kuskov's suggestion and Vania Kashperuk's article, I put down this simple job that iterates Tables and Extended Data Types groups members in a given shared project.
static void tmxIterateProjectGroupMembers(Args _args)
{
    #aot
    #properties
    Str                         projectName = "tmxEDI999";
    ProjectNode                 projectNode;
    ProjectGroupNode            ddProjectGroupNode;
    ProjectGroupNode            edtProjectGroupNode;
    ProjectGroupNode            tblProjectGroupNode;
    ProjectListNode             projectListNode;
    TreeNode                    memberTreeNode;              
    TreeNode                    projectTreeNode;
    TreeNodeIterator            projectIterator;
    
    if(projectName)
    {
        // find all shared projects
        projectListNode = SysTreeNode::getSharedProject();
        // find project with a given name
        projectNode = projectListNode.AOTfindChild(projectName);
        // open it in a separate window in AOT
        projectTreeNode = projectNode.getRunNode();
        // this is the key point after which we can iterate group members
        projectNode = projectNode.loadForInspection();
        // get nested nodes for appropriate names
        ddProjectGroupNode = projectNode.AOTfindChild('DataDictionary');
        edtProjectGroupNode = ddProjectGroupNode.AOTfindChild('Extended Data Types');
        tblProjectGroupNode = ddProjectGroupNode.AOTfindChild('Tables');
        
        // tables
        projectIterator = tblProjectGroupNode.AOTiterator();
        memberTreeNode = projectIterator.next();

        while(memberTreeNode)
        {
            info(strFmt("%1 %2", memberTreeNode.AOTname(), memberTreeNode.treeNodeName()));
            memberTreeNode = projectIterator.next();
        }

        // extended data types
        projectIterator = edtProjectGroupNode.AOTiterator();
        memberTreeNode = projectIterator.next();

        while(memberTreeNode)
        {
            info(strFmt("%1 %2", memberTreeNode.AOTname(), memberTreeNode.treeNodeName()));
            memberTreeNode = projectIterator.next();
        }
    }
}
The key method is loadForInspection. Happy iterating!

Wednesday, October 8, 2014

C# code to test EDI-XML transformation

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using Microsoft.Dynamics.IntegrationFramework.Transform;
using tmxEDITransformsX12.SharedXSD;
using tmxEDITransformsX12.Transform820XSD;

namespace TransformTest
{
    class Program
    {
        static void Main(string[] args)
        {
            FileStream input = new FileStream("C:\\Test.edi", FileMode.Open);
            FileStream output = new FileStream("C:\\Output.xml", FileMode.OpenOrCreate);

            tmxEDITransformsX12.Transform820 transform = new tmxEDITransformsX12.Transform820();

            transform.Transform(input, output, "");


        }
    }
}

Wednesday, September 24, 2014

How to find in AOT all objects named like...

In fact the standard Find tool in AOT works well if you really know how to fill in all these parameters before launch it. Sometimes it is easier to use another options.

Let's say we need to find all the forms that contain 'lookup' word in the end of their names. Easy? Yes, it is! You can go directly to the SysModelElement table and work with it as with any other table: Ctrl-G to switch grid filter, 11 for Form element type, then *lookup in the name, and that's it.



This is rapid way to know the objects names but it is not possible to jump to them in AOT. Here we go with the second option. Just create a new project and filter criteria Element type name and Name for the objects. 


Simple, fast and will keep the search results for future recalls.



Happy searching in AOT!