Wednesday, 7 March 2018

ReferenceGroupControl Creation

https://devmusings.blog/2018/02/09/reference-group-controls-in-dynamics-365-for-operations/

Microsoft introduced the reference group control in Microsoft Dynamics AX 2012 that is unchanged in Microsoft Dynamics 365 for Finance and Operations. The control is a hybrid between a regular form control and a form group. It acts as a control during form design. At runtime, it appears as a group of controls. There are several reasons to use a reference group control.
  • It simplifies adding a look up to a record identifier field (RefRecId).
  • It reduces the need for coding display methods.
  • Users can filter and sort on fields exposed by the control.
  • It provides additional lookup logic (filters data if the entry is ambiguous).
  • It is smart enough to resolve table inheritance.
With these advantages, there are a couple disadvantages. As the fields store record identifiers, it is more difficult to analyze data using the table browser or SQL. Additionally, these goodies require additional setup such as creating a special data type and setting properties.
For this example, we have a pair of tables: MK_Pirates and MK_Ninjas.
For both tables, we have a unique index using the PirateId and NinjaId fields. Both indexes have the Alternate Key property set and the ReplacementKey property is the respective index.
We need to create a table and form to record encounters between the pirates and ninjas. The table should record a pirate, a ninja, and a result. Our MK_PirateVsNinja table already has the fields for pirate and result. We will be adding the field for ninja.
The first step is to create a new data type for the ninja field. For this, we create a new Int64 data type that extends RefRecId. We name this data type MK_NinjaRecId. On this data type, we set the ReferenceTable property to our MK_Ninja table.
After creating the data type, we need to add it to our encounter table. We can drag the MK_NinjaRecId onto the MK_PirateVsNinja table to create the field. Visual Studio should ask if you want to create a relationship. Click Yes and rename the newly created field to something more readable.
In the editor for the form, drag the newly created Ninja field into the grid. This will create a reference group control. Compile the project and open the form in the browser. We should be able to select pirates and ninja using the drop-down buttons.
Even though the underlying table stores record identifiers, the form shows the identifier of the pirates and ninjas.
Unfortunately, seeing just the identifier is not helpful in this case. We need to show the pirate’s name and ship and ninja’s name and dojo on the form. There are a couple ways we can accomplish this. One way is to modify the AutoIdentification field group on the parent tables. This field group populates automatically based on the replacement key. We can modify it to show the fields we want.
Another way is to change the ReplacementFieldGroup property on the reference group control. This property, that defaults to AutoIdentification, determines which field group from the parent table to display.
After changing either the AutoIdentification field group or the ReplacementFieldGroup property, the form will change to display our intended fields from the parent tables.

Thursday, 21 December 2017

AX7 Dimensions

My current requirement is, we are importing transactions from file. In which MainAccount, Dept and other dime values are existing. By using these values I need to create LedgerDimension and DefaultDimension. 
I tried to use “TMSInvoiceApproval.findOrCreateDynamicDimension(DimensionDynamicAccount _account, DimensionDynamicAccount _dimensions)” method. But couldn’t able to pass parameter values in required format.

LedgerDefaultDimensionValueSet generateDefaultDimension(str _costCentre)
{
List dimensionValue = new List(Types::Class);

DimensionNameValueListContract dimensionNameValueListContract = new DimensionNameValueListContract();
DimensionStorageResult dimensionStorageResult;
DimensionAttributeValueContract dimensionAttributeValueContract;

//#costcentre is the macro for the name of CostCentre dimension attribute

dimensionAttributeValueContract = DimensionAttributeValueContract::construct(#costcentre, _costCentre);
dimensionValue.addEnd(dimensionAttributeValueContract);

dimensionNameValueListContract.parmValues(dimensionValue);

dimensionStorageResult = DimensionNameValueListServiceProvider::newForDimensionNameValueListContract(dimensionNameValueListContract).resolve();

return dimensionStorageResult.parmSavedRecId();
}

inventJournalTrans.DefaultDimension = generateDefaultDimension(“8099”);
=================================================================

In Ax 2012 we have class AxdDimensionUtil that is obsolete in Ax 7. The same should be happen with LedgerAccountDimensionResolver class in AX 7.

     DimensionAttributeValueSetStorage dimensionAttributeValueSetStorage;
        DimensionAttribute dimensionAttribute;
        DimensionAttributeValue dimensionAttributeValue;
        DimensionDefault dimensionDefault;
        LedgerDimensionAccount ledgerDimensionAccount;
        DefaultDimensionIntegrationValues   dimensionNames;
        DimensionDisplayValue               dimensionValues;
        DimensionDynamicAccount             dynamicDimension;

        dimensionAttributeValueSetStorage = new DimensionAttributeValueSetStorage();
        
        dimensionNames = DimensionAttribute::find(DimensionAttribute::getWellKnownDimensionAttribute(DimensionAttributeType::MainAccount)).Name;
        dimensionValues = '110110';

        dimensionNames += SystemParameters::getChartOfAccountsDelimiter();
        dimensionValues += SystemParameters::getChartOfAccountsDelimiter();

        dimensionNames += "BusinessUnit";
        dimensionValues += strReplace('001', SystemParameters::getChartOfAccountsDelimiter(), '\\' + SystemParameters::getChartOfAccountsDelimiter());

        dimensionNames += SystemParameters::getChartOfAccountsDelimiter();
        dimensionValues += SystemParameters::getChartOfAccountsDelimiter();

        dimensionNames += "Department";
        dimensionValues += strReplace('022', SystemParameters::getChartOfAccountsDelimiter(), '\\' + SystemParameters::getChartOfAccountsDelimiter());
        LedgerAccountDimensionResolver ledgerAccountDimensionResolver = LedgerAccountDimensionResolver::newResolver(dimensionValues);
        ledgerAccountDimensionResolver.parmDimensionFormat(dimensionNames);
        dynamicDimension = ledgerAccountDimensionResolver.resolve();
===================================================================

Aging Report

Need to add custFactored Status Field  ( which is enum and maintain as range). In Aging Report :

1.     Added in Contract Field  - Enum(SOLD,NOTSOLD,BLANK)  CustFactored .
2.     Add CustFactored field in Temp Table  “CustAgingReportTmp”, CustTmpAccountSum
3.     CustAgingReportDP  modifications
i)                   Process Report

custVendBalanceList = CustVendBalanceList::construct(SysModule::Cust,
                                                                contract.parmDateTransactionDuedate(),
                                                                contract.parmExcludeZeroBalanceCustomer(),
………………………………………………………….
contract.parmFactorStatus()); //Added Line


ii)                insertCustAgingReportTmp()

insert_recordset custAgingReportTmp
           (Balance01, Balance02, Balance03, Balance04, Balance05, Balance06, Balance07,----------------------------
            HeadingAgingBucketDescription06, HeadingAgingBucketDescription07, Balance,CustFactored,SortOrder)//Added Line

                select
                    Balance01, Balance07, Balance06, Balance05, Balance04, Balance03, Balance02,
------------------------------------------
                    headingAgingBucketDescription06, headingAgingBucketDescription07, balance,CustFactored//Added Line

                from tmpAccountSum
                        join SortOrder
                        from custVendTransAging
                            where tmpAccountSum.AccountNum == custVendTransAging.AccountNum;


4.     Need to retrieve value Cust Factored sent from DP class. – CustVendBalanceList and pass to CustBalanceList

i)                   Construct ()
public static CustVendBalanceList construct(SysModule               _sysModule,
                                            DateTransactionDuedate  _dateTransactionDuedate,
                                            ……………………………………………………………………………………………………………………..
                             NoYes                   _excludeNegativeBalance = NoYes::No,
                             CustFactorStatus        _factorStatus   =   CustFactorStatus::Blank

Since only related to Cust Module my modification I passed only for Cust Case
case SysModule::Cust :
            switch(_dateTransactionDuedate)
            {
                case DateTransactionDuedate::DocumentDate :
                        balanceList = new CustBalancelistDocumentDate(  _range,
-------------------------------------------------------------------------------                                                                       
                                                                        _factorStatus);
                        break;

                case DateTransactionDuedate::TransactionDate :
                        balanceList = new CustBalancelistTransactionDate(   _range,
                                                                            ---------
                                                                            _factorStatus);
                        break;

                default :
                        balanceList = new CustBalanceList(  _range,
                                                            -------
                                                            _factorStatus);
                        break;
            }
            break;


ii)                New ()

void new(PositiveDays               _interval,
         ------------------------------------
         CustFactorStatus  _factorStatus  =   CustFactorStatus::Blank)


5.     Need to Catch the value from CustBalanceList
i)                   New()

void new(PositiveDays               _interval,
         ---------------------------------------
         NoYes                      _excludeNegativeBalance = NoYes::No,
         CustFactorStatus           _factorStatus   =   CustFactorStatus::Blank)

super(_interval, _transactionDate, _zeroDate, _payments, _period, _direction, _agingBucket,
        _printReversed, _details, _excludeZeroBalance, _excludeNegativeBalance,_factorStatus);

agingCalculation = CustVendAgingCalculation::construct(   SysModule::Cust,
                                                        -----------------
                                                        inclTransWithNoBillingClass,
                                                        _factorStatus);

ii)                insertIntoTmpAccountSum()

Since it need to insert value , not update in any of bucket…

if (_withAmountCur)
    {
        insert_recordset custTmpAccountSum
            (AccountNum, CurrencyCode, BillingClassification, InvoiceId, TransDate, Txt, Voucher,CustFactored,Balance01, Balance01Cur, Name, GroupId)
                select AccountNum, CurrencyCode, BillingClassification, InvoiceId, TransDate, Txt, Voucher,CustFactored,sum(Amount), sum(AmountCur)
                    from _agingCalculatedTmp
                        group by AccountNum, CurrencyCode, BillingClassification, TransDate, InvoiceId, Voucher,CustFactored,Txt
                join Name, GroupId from customers
                    group by Name, GroupId
                        where _agingCalculatedTmp.AccountNum == customers.AccountNum;




Methods where  actually they include the transactions :
  [s]    \Classes\CustAgingCalculation\selectTransactions 
  \Classes\CustAgingCalculation\selectDetailsOfTransactions



  [s]    \Classes\CustAgingCalculation\selectTransactions                                                       13
 
update_recordSet agingProcessingTmp setting
        IsPayment = true
            where agingProcessingTmp.AmountCur < 0
                exists join custTrans
                    where custTrans.RecId == agingProcessingTmp.TransRecId
                        && !custTrans.Invoice;


[s]    \Classes\CustAgingCalculation\selectDetailsOfTransactions                                               1
case DateTransactionDuedate::DueDate:

            update_recordSet agingProcessingDetailsTmp setting
                TransDate = custSettlement.DueDate
                    join agingProcessingTmp
                        where agingProcessingTmp.RecId == agingProcessingDetailsTmp.ProcessingRecId
                    join custSettlement
                        where agingProcessingTmp.SettlementRecId == custSettlement.RecId
                            && custSettlement.TransDate > balanceAsOfDate
                            && custSettlement.DueDate > dateNull();

            update_recordSet agingProcessingDetailsTmp setting
                TransDate = custTransOpen.DueDate
                    join agingProcessingTmp
                        where agingProcessingTmp.RecId == agingProcessingDetailsTmp.ProcessingRecId
                    join custTransOpen
                        where agingProcessingTmp.TransOpenRecId == custTransOpen.RecId
                            && custTransOpen.TransDate <= balanceAsOfDate
                            && custTransOpen.DueDate > dateNull();
            break;

        case DateTransactionDuedate::DocumentDate:
            update_recordSet agingProcessingDetailsTmp setting
                TransDate = custTrans.DocumentDate
                    join agingProcessingTmp
                        where agingProcessingTmp.RecId == agingProcessingDetailsTmp.ProcessingRecId
                    join custTrans
                        where agingProcessingTmp.TransRecId == custTrans.RecId
                            && custTrans.TransDate <= balanceAsOfDate
                            && (!custTrans.Closed || custTrans.Closed >= balanceAsOfDate)
                            && custTrans.DocumentDate > dateNull()
                    exists join customersVendors

                        where custTrans.AccountNum == customersVendors.AccountNum;


1.)    Remove the date 12/31/2154 – this occurs when the unit setup is ‘0’ and interval ‘unlimited’ in the aging period definition
A.      To navigate this:
AP>> setup>> Aging Period Definitions.




If date is 2154/12/31 - then the value should be null :
---------------------------------------------------------

=iif(Fields!Heading14.Value = FormatDateTime(CDate("2154/12/31"),2), "",Fields!Heading14.Value)

If we want Date  from Date time :
---------------------------------------

Format(Fields!Heading7DateValue.Value, Microsoft.Dynamics.Framework.Reports.BuiltInMethods.GetExtendedDataTypeFormat("Transdate", Parameters!AX_RenderingCulture.Value))


=======================================================

=iif(Fields!Heading14.Value = FormatDateTime(CDate("2154/12/31"),2), "",Format(Fields!Heading14.Value, Microsoft.Dynamics.Framework.Reports.BuiltInMethods.GetExtendedDataTypeFormat("Transdate", Parameters!AX_RenderingCulture.Value)))
=====================================================


Monday, 18 December 2017

X++ code to read CSV files in Dynamics 365 for operations

Reading data from csv files is a very common development requirement for X++ developers. In Dynamics 365 for operations there are some interesting concepts, as opposed to AX2012. The application runs on cloud and the client is in web. In such scenarios the way to read files from local system is a 2 step process.
Step 1: Upload the file from local machine to Microsoft cloud (Azure) storage space called as Azure blob.
Step 2: Read the file from Azure blob to retrieve the data.
 The standard API's are used here do the required magic. I have tried to breakdown the functionality of the calls. 
Step 1 is done by the below command where the file is uploaded from local machine to azure blob


 
Step 2 is done by opening the IO stream from the Blob and reading it from the IO class

 

Rest of the job is almost similar to AX2012 pattern.
 
To test the job we create a sample csv file 

 
 
On running the job a file picker will be shown, where you can browse the file.



 
The progress of uploading it to cloud space is shown in the progress bar


 
 
 
 
Infolog is shown with the data on the file 


 
 

Worth mentioning that there are various other IO classes, provided out of the box which have the below associations
 

 
 
The file upload classes have different strategies and the below classes are available out of the box. For more information refer to nice articles mentioned in the references:


To quickly reuse the code here it goes 

class RGReadSample
{        
    /// 
    /// Runs the class with the specified arguments.
    /// 
    /// The specified arguments.
    public static void main(Args _args)
    {        
        AsciiStreamIo                                   file;
        Array                                           fileLines;
        FileUploadTemporaryStorageResult                fileUpload;

        fileUpload = File::GetFileFromUser() as FileUploadTemporaryStorageResult;
        file = AsciiStreamIo::constructForRead(fileUpload.openResult());
        if (file)
        {
            if (file.status())
            {
                throw error("@SYS52680");
            }

            file.inFieldDelimiter(',');
            file.inRecordDelimiter('\r\n');
        }

        container record;
        while (!file.status())
        {
            record = file.read();

            if (conLen(record))
            {
                info(strFmt("%1 - %2",conPeek(record,1),conPeek(record,2)));
            }
        }

        info("done");
    }

}