Friday, November 21, 2014

How to print labels in multiple languages

This is a short job to print labels in English US and English Canada. The prefix defines the label file.

static void printLabels(Args _args)
{
    int     labelNum = 1;
    int     labelIndex;
    int     labelMaxNum = 216;
    str     labelPrefix = "@CGI";
    str     labelStr;
    str     languageIdEN = 'en-us';
    str     languageIdFR = 'en-ca';

    for (labelIndex = labelNum; labelIndex <= labelMaxNum; labelIndex++)
    {
        labelStr = labelPrefix + int2str(labelIndex);
        labelStr = strFmt("%1 : %2 /// %3", labelStr,
                                            SysLabel::labelId2String2(strFmt("%1", labelStr), languageIdEN),
                                            SysLabel::labelId2String2(strFmt("%1", labelStr), languageIdFR));
        info( labelStr);
    }
}

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, "");


        }
    }
}