Quantcast
Channel: Dynamics 365 FO / AX - thoughts on application development and customizations
Viewing all 54 articles
Browse latest View live

Article 4

$
0
0
Just needed to save some code I have been working on in my blog.

This code is used for basically deconstructing ledger dimensions and default dimensions to be able to merge / overwrite dimension attribute values in the ledger dimension.

My scenario is that during the processing of a ledger allocation, I have an "original" transaction that is being allocated. My destination transaction (the transaction that allocates the original transaction to a new ledger dimension) will inherit all the ledger dimensions of the original transaction BUT will have some of it's dimension attribute values overwritten, by a default dimension set up on the ledger allocation rule destination.

Basically I deconstruct the ledger dimension of the original transaction and put all the found dimensions into a map.

I the deconstruct the default dimension of the ledger allocation rule destination, and find the matching entries in my map, to replace them.

When my "merged" map is complete, I prepare a list of DimensionAttributeValueContract objects that is fed in to a LEdgerAccountContract object.

This is then used when calling DimensionsServiceProvider::BuildDimensionStorageForLedgerAccount to create a DimensionAttributeValueCombination record that can be attached on my LedgerJournalTrans record in the LedgerDimension field.



///
/// Finds or creates a DimensionAttributeValueCombination record

///

///

/// GeneralJournalAccountEntry record

///

///

/// LedgerAllocationRuleDestination

///

///

/// Recid of DimensionAttributeValueCombination record

///

///

/// Used to ensure that the correct main account / dimension combination for allocation ledgerJournalTrans are available

/// The combination is constructed using dimensions inherited from the "original" generealAccountJournalEntry record that is being allocated

/// and the default dimension of the ledgerallocationruleDestination. The dimensions from the ledgerAllocationRuleDestination overrides the

/// dimensions of the original generealAccountJournalEntry if set.

///


private recId findOrCreateDimAttrValueCombination(LedgerJournalTrans _sourceTrans,LedgerAllocationRuleDestination _ledAllocationRuleDest)


{

// These objects are used for constructing a ledgerdimension

LedgerAccountContract ledAccountContract;

DimensionAttributeValueContract dimAttrValueContract;

List dimAttrValueObjects;

Map commonAttributeMap;

MapIterator commonAttributeMapInterator;


DimensionStorage dimStorage;

// Deconstruction variables ledgerdimension

int hierachyCount,hierachyIndex;

int segmentCount,segmentIndex;

DimensionStorageSegment dimStorageSegment;

DimensionAttribute orgTransDimAttribute;

DimensionAttributeValue orgTransDimAttributeValue;


// Deconstruction variables defaultdimension for ledgerallocationruledestination

DimensionAttributeValueSetStorage dimAttrValueSetStorage;

int ruleDestCount,ruleDestIndex;

// Common attribute map object

commonAttributeMap = new Map(Types::Int64,Types::Int64);


// Construct dimensions storage object for desconstructing dimension values of original trans

dimStorage = DimensionStorage::findById(_sourceTrans.LedgerDimension);


hierachyCount = dimStorage.hierarchyCount();



// Deconstruction original trans and constructing a List of dimAttrValueContract objects that can be used for finding/creating a

// DimensionAttributeValueCombination record

for(hierachyIndex=1;hierachyIndex<=hierachyCount;hierachyIndex++)

{

segmentCount = dimStorage.segmentCountForHierarchy(hierachyIndex);

for(segmentIndex=1;segmentIndex<=segmentCount;segmentIndex++)

{

// Get segment

dimStorageSegment = dimStorage.getSegmentForHierarchy(hierachyIndex,segmentIndex);

if (dimStorageSegment.parmDimensionAttributeValueId() != 0)

{

orgTransDimAttribute = DimensionAttribute::find(DimensionAttributeValue::find(dimStorageSegment.parmDimensionAttributeValueId()).DimensionAttribute);

orgTransDimAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValue(orgTransDimAttribute,dimStorageSegment.parmDisplayValue());

commonAttributeMap.insert(orgTransDimAttribute.RecId,orgTransDimAttributeValue.RecId);

}

}

}



// Deconstruction ledger allocation rule destination and overriding dimensionattribute values for original transaction dimension with the default dimension

// from the allocation rule destination



// Construct DimensionAttributeValueSetStorage object for deconstructing dimension values of ledgerAllocationRuleDestination

dimAttrValueSetStorage = DimensionAttributeValueSetStorage::find(_ledAllocationRuleDest.ToDefaultDimension);

ruleDestCount = dimAttrValueSetStorage.elements();


// Traverse DimensionAttributeValueSet and "override" values in common attribute map

for(ruleDestIndex=1;ruleDestIndex<=ruleDestCount;ruleDestIndex++)

{

if (commonAttributeMap.exists(dimAttrValueSetStorage.getAttributeByIndex(ruleDestIndex)))

{

// Remove dimension already gathered

commonAttributeMap.remove(dimAttrValueSetStorage.getAttributeByIndex(ruleDestIndex));

// Insert overridden dimension

commonAttributeMap.insert(dimAttrValueSetStorage.getAttributeByIndex(ruleDestIndex),dimAttrValueSetStorage.getValueByIndex(ruleDestIndex));

}

else

commonAttributeMap.insert(dimAttrValueSetStorage.getAttributeByIndex(ruleDestIndex),dimAttrValueSetStorage.getValueByIndex(ruleDestIndex));

}



// Build list of DimensionAttributeValueContract objects

// Initialize list of DimensionAttributeValueContract objects

dimAttrValueObjects = new List(Types::Class);

commonAttributeMapInterator = new MapIterator(commonAttributeMap);

// Traverse "common set" of attributes for main account

while (commonAttributeMapInterator.more())

{

// Prepare DimensionAttributeValueContract objects for DimensionServiceProvider object

dimAttrValueContract = new DimensionAttributeValueContract();



orgTransDimAttribute = DimensionAttribute::find(commonAttributeMapInterator.key());

orgTransDimAttributeValue = DimensionAttributeValue::find(commonAttributeMapInterator.value());



// Inherit dimension from original transaction

dimAttrValueContract.parmName(orgTransDimAttribute.Name);

dimAttrValueContract.parmValue(orgTransDimAttributeValue.getValue());

dimAttrValueObjects.addEnd(dimAttrValueContract);

commonAttributeMapInterator.next();

}



// Initialize LedgerAccountContract

ledAccountContract = new LedgerAccountContract();

ledAccountContract.parmMainAccount(DimensionStorage::getMainAccountFromLedgerDimension(_sourceTrans.LedgerDimension).MainAccountId);

ledAccountContract.parmValues(dimAttrValueObjects);

return DimensionServiceProvider::buildDimensionStorageForLedgerAccount(ledAccountContract).save();

}

As always - If you use this code, you do so at your own risk.

Dynamics AX 2012: Error occurs when importing .xpo-file.

Dynamics AX 2012 Excel Add-in document services and queries with missing relatios

$
0
0
While working on an assignment where I among other things had to prepare a new query for use with document services and the Excel add-in.

The query consisted of three nested data sources, where two was standard tables and the third was a new table which I had added to the system.

First version worked just fine. I exposed the query through document services and I could get data in Excel.

The for some reason I decided to change the relations on the table I had added, for reasons that I forget.

However suddenly my query was not showing up in the pick list, when I using the "Dynamics AX" / Add Data button i Excel.

The reason ? I had messed up the relation from my new table to the standard table, making the relation empty.

The Excel add-in reacts by simply not showing the now invalid query in the list, but it does not warn you that something is wrong.

I got a clue to this, by trying to use "Add table" instead going directly for my new table.
Then Excel add-in showed me an error message, where it stated that something was wrong in the relation.

I proceeded to correct the table relation, and voila, my query was back in action to serve as a Document data source.

So if you experience that your query will not show up in the Excel add-in try checking that the table relations of the tables used in the query, or the relations used in the query are valid.

Fixed assets DAX 2012: Missing value models in lookup

$
0
0
Having trouble with missing value models in lookup in Fixed Assets module.
Try running the class

AssetRebuildTables


The EDT AssetBookMergeId defines either a value model or a depreciation book.
This EDT is related to a table called AssetBookMergeLookup.

I experienced that this table had been "corrupted", so that one of the value models were not available in the lookup.

I ran the AssetRebuildTables class from the AOT, and voila.
The table was rebuilt for me.

Article 0

$
0
0
Yes I know it's small chance of ever winning, but if you don't play you have no chance of winning.

Firebrand Training is giving you the chance to win free training for a life time, and who wouldn't want that as a price. :) Please click Firebrand Trainingto enter the competetion your self.

Showing hidden fields in the table browser

$
0
0
A request for the possibility of showing the contents of hidden fields in Ax tables, using the table browser, made me do a google search, to avoid reinventing the wheel.

Sure enough Vilmos Kintera had adressed the problem earlier on http://daxrunbase.blogspot.dk/2010/04/non-visible-fields-in-table-browser.html. And he has done good work. :) Keep it up Vilmos.

Running the customized table browser seemed to work fine in AX 2009, however I ran into issues using it in AX 2012. I tested the customized table browser on the table CustTrans, which was the table I first found to contain hidden fields and I got a run-time error.

Investigating the problem I found that the reason in seemed to run fine on AX 2009 (using CustTrans table) but failed in AX 2012 on the same table, was due to a small issue that Vilmos hadn't adressed, namely Extended Data Types defined as Arrays.

The .ds datasource on the SysTableBrowser form had it's active method customized to populate the new lower grid, with rows showing the contents and "identity" of each hidden field. The method contains a switch case construction takling care of all intrisic types, and the default (no type found) simply attempts to convert the field value of the field in question to a string. However the issue was that fields based on Extended Data Types defined as arrays were not handled causing a run-time error.

I hacked this to make a further branch in the default case, checking if the arrayindex of the field in question is greater than 1, indicating that the field is based on an EDT defined as an array. If the field in question is an array, the hack iterates each array element and creates a row in the lower grid for each found array element.

The reason why everything seemed to work fine i AX 2009 was that DIMENSION fields were EDT's defined as arrays but in AX 2012 everything changed regarding financials dimensions, as you might know. In 2009 and previous versions Dimension fields were never hidden, as they were of course used. But in 2012 the old Dimension-fields are prefixed with DEL_ and put in the Obsolete... configuration key to be deleted in next upgrade.

The problem I encountered in CustTrans were due to the abovementioned. The customized table browser ran fine in AX 2009 because the Dimension field was in use and not hidden. However in AX 2012 this was not so, so the code could not handle the hidden Del_Dimension field (or indeed any array based field).

However the code WOULD have failed in 2009 also if anyone had browsed a table with a hidden array field. I tested this by setting the visible property of the Dimension field in an AX2009 to false, and browsing with Vilmos' customization and the result was a run-time error.

So here is my own modified version of Vilmos' tablebrowser.

AX 2012 - .xpo-file

AX 2009 - .xpo-file

Disclaimer: Use this code at own risk !!! This blogger can in no way be made responsible for data loss or damages that follows using this code.

Very small and simple tool to use when comparing code in different AX environments

$
0
0
Some time ago I was was given the task of determining the differences in codebase between three different AX environments, running AX 2012 RTM.

You might ask why would this be necessary, if you follow best practices that code is always moved from DEV to TEST, from DEV TO STAGING, and from STAGING to PRODUCTION, the only differences would be new ?

Well, sometimes it is just not possible to test and correct a data problem that the users have in PRODUCTION by having the data rolled back into DEV within the timeframe given to solve the problem, and this leads to someone making hotfixes in the code directly in PRODUCTION. I know, I know - NOT recommendable.

However the situation was that noone was exactly sure that the codebase in the environments were aligned, and wanted to know if there were any differences and where.

Enter Araxis Merge, which is a wonderful tool for comparing and merging text-files. The advantage Araxis has over Dynamics AX's own compare tool are several:

  • Better visual indication of differences
  • Better merging possibilities
  • Three way comparison
  • Three way comparison of files in directories


Especially the last item on the above list came in VERY handy in my case.

I wrote a very small AX class, that simply scanned the AOT-layers that had been used for customization for any customized elements. When finding one it simply exported it naming the file with the following convention: "Layer"-"ElementType"-"ElementName".xpo

This produces a number of .xpo-files (one for each customization found in the AOT). Now I ran the class for each of the three environments producing files for DEV, TEST and PROD.

I then put the files from each environment in a separate folder on my disk, and asked Araxis to do a folder comparison. And voila:

(note that the example shown in the picture only contains a two way folder comparison).











Araxis shows you a list where it has first compared the folders and it shows if there are files missing (customized elements) in one of the folders (environments) with a grey or green color. The purple color indicates that the element is there in both environments, but if the element is different in one environment it has been marked with a small red square. On these lines you can click on the element, to open a file compare, where you have merging possibilities.











Here you can find the class that does the export.

Ax Class:AOTExport

In future it will be exciting to see if Microsoft Dynamics Lifecycle Services will bring possibilities/tools regarding issues like this one.

Modal forms in Dynamics AX

$
0
0
Some years ago, I wrote on this blog of a method to make a form modal in the AX client. The solution was made on Axapta 3.0 and involved calling winapi unmanaged. Someone commented that the method I described was bad code, because he had experienced a computer crash while debugging something that involved the code.

Mindaugas Pocius has suggested a method in his Dynamics AX cookbook books Dynamics AX Cookbookswhere the windowtype is set to Popup. This ensures that the form is on top but not that the form you want to be modal, is the only one that is active, so using Mindagaus' method, you are able to leave the form that is "modal".

Now a colleague of mine has found a very simple solution. His scenario was this. From a form you can click a button that opens an other form. This form that is opened must be ontop and the only form in focus and active until you either click OK or CANCEL in this form.

The calling form includes a button, that open the form to be modal. The called form / modal form includes a simple command ( element.wait(true); )in the run method of the form:

public void run()
{
    super();
    element.wait(true);
    // Execution will resume at this point, only after
    // the user has closed the form.
}


So in essence the form you that is modal is in essence waiting for it self to be closed, before the execution path is returned to the calling form.

How to work around best practice error: "A form that is not associated with other forms should be linked to at least one menu item"

$
0
0
How does one work around the Best practice error: "A form that is not associated with other forms should be linked to at least one menu item", when developing a custom lookup form ?

Developing a possibility of a custom setup on the table ReqSitePolicy, that allows for calculation of "External Invent Onhand level", I added two new extended data types:

ItemSearchInventSiteId exending InventSiteId
ItemSearchInventLocationId extending InventLocationId

and three new fields:

ItemSearchShowAvailInventDataAreaId based on EDT SelectableDataArea
ItemSearchShowAvailInventSiteId based on EDT ItemSearchInventSiteId
ItemSearchShowAvailInventLocationId (based on a EDT ItemSearchInventLocationId)

to the ReqSitePolicy table.

To be able to make lookups for ItemSearchShowAvailInventSiteId and ItemSearchShowAvailInventLocationId fields based on the legal entity chosen in the ItemSearchShowAvailInventDataAreaId field (even though the currently active legal entity might be something else), I prepared two custom lookup forms for the EDTs ItemSearchInventSiteId and ItemSearchInventLocationId. The querys of the custom lookup forms have the property CrossCompanyAutoQuery set to YES.

The custom lookup forms uses the legal entity chosen in the ItemSearchShowAvailInventDataAreaId to make a crosscomapny lookup for InventSiteId and InventLocationId, clearing dynalinks and doing:

this.query().addCompanyRange(dataAreaIdCriteria);

in the executeQuery-method of the datasource of the lookup form.

However compiling the lookup forms resulted in the best practice error: "A form that is not associated with other forms should be linked to at least one menu item".

Well, to solve that problem open the lookupform in the - AOT-path:

Form / "custom lookup form name" / Designs / Design.
Set the "Style" property to "Lookup".

And poof the Best Practice error has gone away.

My apologies.

$
0
0
I recently discovered that I have not been attending my mail box blog@fasor.dk. My sincerest apologies to everyone who have written to this mail box and gotten no answer. The mail box is now set up on my windows phone, and I will be answering sent to it.

Finding the cause of an incremental CIL-build error

$
0
0
I learned something new from one of my colleagues today, which is always nice.

I have often been irritated by the Dynamics AX 2012 CIL-builds ability to be uninformative.
The incremental CIL-build does not always tell you why it is unable to finish an incremental build, and up until today my procedure for fixing problems to be able to cil-build incrementally has been to run a complete x++ compile, which takes quite a long time.

You can examine the cause(s) of the CIL-build failure by doing the following:

1) Login on the AOS-server.
2) Find the AOS server folder
3) Go to the folder of the instance where you experienced CIL-build problems.
4) Goto folder Bin\XppIL
5) Find the file Dynamics.Ax:Application.dll.log (an example of a full path for the file could be C:\Program Files\Microsoft Dynamics AX\60\Server\DEV_AX2012\bin\XppIL\Dynamics.Ax:Application.dll.log)

In this file you can see the compile-errors which caused the incremental CIL-build to fail.

Thank God for technical sparring. :-)

Faster compilation for AX2012 - nice

$
0
0
http://blogs.msdn.com/b/axtools/archive/2013/11/04/parallel-x-compilation-for-microsoft-dynamics-ax2012-r2-in-cumulative-update-7.aspx http://blogs.msdn.com/b/axtools/archive/2013/04/30/ax2012-r2-hotfix-available-improves-compile-speed.aspx http://daxmusings.codecrib.com/2013/11/what-you-need-to-know-about-15-minute.html

A little extension for diagnosing tax code combination errors

$
0
0
In Dynamics AX 2012 if General Ledger module is setup to validate Check sales tax by entering either "Warning" or "Error" in the "Check sales tax groups" field, then Dynamics AX will throw an error (stop execution) or issue a warning in the infolog if it encounters a tax setup error when doing calculation of tax.



The error looks like this:


As you might know the tax calculation is based on getting the intersecting tax codes connected to the item tax group and the tax group of a given source document line.

In this case I encountered we wanted to diagnose the origins of the error, because the error occurred when changing a customer group, which we thought was a bit odd.

Digging a bit deeper, the error was not related to changing the customer group, but instead related to the following recalculation of credit limit on the customer that Dynamics AX wanted to perform.

I traced the error to the class Tax and the method insertInterSection.

Seing that the Tax class is a super class the reason for the not very informative error message became clear. The insertIntersection method is inherited by the sub classes in the Tax class hierachy and therefor contains logic that is not module specific, so there are no references to the origin-lines (e.g. sales lines og purch lines) that the tax group set up comes from.

To get info about the origins of the error I did the following:

I added a new method to be used when an error is to be reported:

private str invalidTaxCodeCombErrortxt(ErrorTxt _errorTxt)
{
DictTable dictTable;
Common dCommon;
ErrorTxt foundInErrorTxtPostfix;

dictTable = new dictTable(this.sourceTableId());
dCommon = dictTable.makeRecord();
select firstOnly dCommon where dCommon.recid == this.sourceRecId();
switch (dCommon.TableId)
{
case tableNum(SalesLine):
foundInErrorTxtPostfix = strFmt(" found in : %1 linje %2",dCommon.(fieldNum(SalesLine,SalesId)),dCommon.(fieldNum(SalesLine,LineNum)));
break;
case tableNum(PurchLine):
foundInErrorTxtPostfix = strFmt(" found in : %1 linje %2",dCommon.(fieldNum(PurchLine,PurchId)),dCommon.(fieldNum(PurchLine,LineNumber)));
break;
default:
foundInErrorTxtPostfix = '';
}
return _errorTxt + '' + foundInErrorTxtPostfix;
}

The method attempts to evaluate a generic tablebuffer to determine what is the source of the tax codes being evaluated, and which cause an invalid combination.
It does so by instantiating a DictTable object using the table id found in the tax class (this.sourceTableId()). It then does a call to the kernel method makeRecord() on the same object to get a tablebuffer.
The we do a select firstonly on this tablebuffer using the recid in (this.sourceTableId()) to get the record that is the origin of the tax groups.

The it evaluates the table id of the found record and constructs and error message text, that can be post fixed to the standard error message.

To notice in the

dCommon.(fieldNum(SalesLine,SalesId))

construct which actually let's you access the fields of a common table buffer.

The InsertIntersection method is changed so that the switch case construct in line 175 looks like this:

switch (this.taxParameters().CheckIntersection)
{
    case CheckTaxGroups::Warning :
        //warning(strFmt("@SYS75213", _taxGroup, _taxItemGroup));
        warning(this.invalidTaxCodeCombErrortxt(strFmt("@SYS75213", _taxGroup, _taxItemGroup)));

        break;
    case CheckTaxGroups::Error :        //warning(strFmt("@SYS75213", _taxGroup, _taxItemGroup));
        warning(this.invalidTaxCodeCombErrortxt(strFmt("@SYS75213", _taxGroup, _taxItemGroup)));
        break;
effectively adding more info to the error message.



This way I was able to pinpoint the source of the problem: An open sales order line on the customer we edited was missing an item tax group.

Show elements in a model from the ABOUT Dynamics AX form

$
0
0
Being in a situation where I needed to determine if a certain AOT-element was included in an installed model, I wonder why Microsoft didn't link the form showing installed models (Help / About Microsoft Dynamics AX / (link) Show installed models) or (Tools / Model management / Models installed) with the form showing Elements in installed models (Tools / Model management / Model elements.

Opening the About Dynamics AX form you are able to access the list of installed models in the application, but you can not drill down to the elements included in the model. You'll have to go to the other form and make a filter to view the elements in a model.

So I quickly made the attached code project, which simply adds a button (Elements) to the Installed models form, so that you can see a list of the elements included in the model.

In the element list you can open the cross reference for each element, to make further drill down as to which elements are using the element included in the installed model with the button (Used by) which opens the cross reference for the chosen element. Be aware however that the cross reference form might "hide" behind the Model windows as it is workspace Window.

ViewElementsInInstalledModel/PrivateProject_JACHS_ElementsInInstalledModel.xpo


As always - use at own risk.

Dynamics AX 2012 ValidTimeState tables and form changing view from current to all

$
0
0
Valid Time State tables are new i AX 2012 a gives the developer the possibility to easily create tables that hold e.g. current setup data for various purposes, and at the same time keeping a "history" of the changes of the data in the table.

For more reading:
http://msdn.microsoft.com/en-us/library/gg861781.aspx


I was tasked with doing a setup table with rates for calculating Vendor Bonus and I chose to base this a valid time state table.

The customer asked for a button on the form, where you maintain the vendor bonus calculation setup data, so you could toggle viewing "Current setup" or "All setup" records (changing the view from actual to all records and vice versa in the form).

I found that you can not change the ValidTimeStateAutoQuery property on the form data source in a form at run-time. It simply does not change anything, so I came up with the following solution:
A boolean class member in the classdeclation method of the form:

boolean showCurrent;

containing which "kind" of view is currently used in the form.

A button that changes the view and on the clicked method of the button:

void clicked()
{
    super();
    showCurrent = !ShowCurrent;
    this.text(showCurrent ? "@SYS38980" : "@NDI892");
    this.helpText(showCurrent ? "@NDI893" : "@NDI894");
    VendorBonusTable_ds.executeQuery();
}

And finally on the executeQuery method on the datasource using the valid time state table:

public void executeQuery()
{
    if (showCurrent)
    {
        VendorBonusTable_ds.query().validTimeStateDateRange(systemDateGet(),systemDateGet());
    }
    else
    {
        VendorBonusTable_ds.query().validTimeStateDateRange(dateNull(),maxDate());
    }
    super();
}


I encountered a strange little quirk when I was testing using jobs that made lookups on my table. Microsoft has introduced a keyword for the select command called validTimeState which is used for specifying on which date you want to select records from the table.

Supposing you have a valid time state table called VendorBonusTable and you write a piece of code that get records on a specific date, you can not use:

VendorBonusTable v;

select validTimeState(systemDateGet()) v;

This will simply not compile.

But you can however do:

VendorBonusTable v;
date d = systemDateGet();
select validTimeState(d) v;

which I personally think is a bit weird.

Dynamics ax 2012 traversing selected records in a form data source

$
0
0
A classical developer challenge in Dynamics AX is to enable a form button when multiple records have been selected in the form by the user.

This usually involves writing some form of loop (for or while or do-while) that starts out with calling _ds.getFirst() and continuing the loop as long as _ds.getNext() returns a tablebuffer.

Well things got a little bit easier in AX 2012. In AX 2012 you can use the MultiSelectionHelper class.

One example is the following that I encountered in AX 2012:

Can you make the customer collection letter print out run for each selected collection record in the
Print/Post collection letters form (Accounts receiveable / Periodic / Collections / Print/Post Collection letters).

If we ignore the possibility for setting up print destination for running each report we can do this in two steps:

1) Change the "Multiselect" property of the "MenuButton" and the "Menuitembutton" in the MenuButton in the form from "Auto" to "Yes".

2) Change the CustCollectionJourController used to run the report to handle multiselected records from the form.
 I chose to implement it this way:

2.1) A new method "isMultipleSelected" was added to the class.
private boolean isMultipleSelected(Args _args)
{
    MultiSelectionHelper msh;
    int i;
    CustCollectionLetterJour cclj;


    if (_args.caller() is SysSetupFormRun)
    {
        msh = MultiSelectionHelper::createFromCaller(_args.caller());
        //msh.
        cclj = msh.getFirst();
        while (cclj)
        {
            if (i>1)
                break;
            i++;
            cclj = msh.getNext();
        }
    }
    return i>1;

}

The method is takes the args passed from the menuitem / form and checks if it is a SysSetupFormRun object. If it is a MultiSelectionHelper object is instantiated.
This can help traverse the selected records in the datasource, and we simply traverse it until we're certain that more than one record is selected.
2.2)

The following code

                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, CollectionLetterNum)).value(selectedRec.CollectionLetterNum);
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, AccountNum)).value(selectedRec.AccountNum);
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, Status)).value(enum2str(selectedRec.Status));
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, CollectionLetterCode)).value(enum2str(selectedRec.CollectionLetterCode));

is substituted for:

if (this.isMultipleSelected(this.parmArgs()))
{
    ds.clearRanges();
    MultiSelectionHelper::createFromCaller(this.parmArgs().caller()).createQueryRanges(ds,fieldStr(CustCollectionLetterJour,CollectionLetterNum));
}
else
{
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, CollectionLetterNum)).value(selectedRec.CollectionLetterNum);
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, AccountNum)).value(selectedRec.AccountNum);
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, Status)).value(enum2str(selectedRec.Status));
                SysQuery::findOrCreateRange(ds, fieldNum(CustCollectionLetterJour, CollectionLetterCode)).value(enum2str(selectedRec.CollectionLetterCode));

}

So MultiSelectionHelper can not only help you traverse the selected record, it can actually build a queryRange for your query based on the selected records in your form by a call to the createQueryRanges method. :)

Nice one.

Dynamics AX 2012 R2 - Export to Excel command button

$
0
0
Today I had some trouble getting a normally simple thing to work in an AX form.

The form consists of to synchronized grids, and the user wanted an "Export to Excel" command button, so the active grid can be exported to Excel.

Normally this is very simple as you just need to add a command button to the form in the ActionPane somewhere and you're home free.

Not this time. It didn't work.

I googled and found this:

http://blogs.msdn.com/b/emeadaxsupport/archive/2009/09/07/how-does-the-export-to-excel-feature-work-under-the-hood.aspx

and put a breakpoint in the performPushAndFormatting method.
I didn't reach my breakpoint and Excel didn't even start.

Then I started to investigate the form.
Maybe the tables of the datasources of the form had some obscure property that needed tweaking, but no.

After pondering this for a while I found that the designer of the form had dropped a field group containing all the fields in the grid. Could that be the problem ?
Clearing the "DataGroup" property of the field group, allowed for dragging all the fields from the field group to the Grid node in the design, and presto, the Export button now worked like a charm.

Lesson learned. If you have a grid in your form, and you want to be able to use an "Export to Excel" command button, using field groups in the grid is a bad idea.

I experienced this on a Dynamics AX 2012 R2.

Returning a weeknumber from the standard calendar lookup form

$
0
0
A customer had a requirement for setting a week number for approximate delivery of purchased goods on shipment as an indication for the sales department of when the goods will be arriving.

An integer field had been introduced on the table in question, and the customer wanted to be able to do a lookup in the standard calendar, but wanted a week number to be returned in stead of a date.

I solved it like this:

1.
For the field that was to contain ETAWeek there was an Extended Data Type called ETAWeek.
On the EDT I put SysDateLookup in the FormHelp property

2.
In the Form SysDateLookup I added a boolean variable in Classdeclaration.

boolean calledFromIntegerField;

3. In the Form SysDateLookup - method init I added check to see if the SysDateLookup form was called from an integer form control.

if (formRun.selectedControl() is FormIntControl) 
    calledFromIntegerField = true;

4.In the Form SysDateLookup - method closeSelect I added an extra else if block in the bottom to handle returning the week number.

else if (calledFromIntegerField)
{
    super(num2str(weekOfYear(selectedValue),0,0,0,0);
}

Now the SysDateLookup works for the ETAWeek field.
UI design wise you can set the "ButtonImage" property on the Extended Data Type ETAWeek mentioned above to the value Calendar to get an calendar icon beside the field.




Forcing the Name field of a salesline to be synchronized to the purchline when using Drop shipment

$
0
0
Using non-stock items in the daily business can be handled in Dynamics AX 2012 by using Direct delivery.

You can use the Button "Direct delivery" from a sales order you have created, to create a matching purchase order.

However if you use the Name field on the salesline to describe the specifications of the item you want to the vendor, the standard functionality does create the matching purchase order lines so that the name of the originating sales line is also used on the sales lines.

Direct deliveries are handled so that the inventory transactions of the salesline are marked against the purchline, so I wrote this small script to be able to get the hang of how to find the direct delivery purchaselines from the saleslines records of a sales order.

static void Job235(Args _args)
{
    SalesLine   salesLine;
    PurchLine   purchLine;
    InventTransOriginSalesLine itosl;
    InventTransOriginPurchLine itopl;
    InventTrans it,it1;
    InventTable iTbl;
    InventHandlingGroup ihg;
   
   
    ttsBegin;
    while select salesLine
        where salesLine.SalesId == "1001135"
        join itosl
        where itosl.SalesLineDataAreaId == salesLine.dataAreaId
           && itosl.SalesLineInventTransId == salesLine.InventTransId
        join it
            where it.InventTransOrigin == itosl.InventTransOrigin
        join it1
            where it1.InventTransOrigin == it.MarkingRefInventTransOrigin
        join itopl
            where itopl.PurchLineDataAreaId == it1.dataAreaId
               && itopl.InventTransOrigin == it1.InventTransOrigin
        join forupdate purchLine
            where purchLine.dataAreaId == itopl.PurchLineDataAreaId
               && purchLine.InventTransId == itopl.PurchLineInventTransId
               && purchLine.Name != salesLine.name
        join itbl
            where itbl.itemid == salesLine.ItemId
         
    {       
        info("***** FØR ********");
        info(strFmt("Salg: %1 %2 %3 %4",salesLine.SalesId,salesLine.ItemId,salesLine.LineNumber,salesLine.name));
        info(strFmt("Indkøb: %1 %2 %3 %4",purchLine.purchid,purchLine.ItemId,purchLine.LineNumber,purchLine.name));
        purchLine.Name = salesLine.Name;
        purchLine.doUpdate();
        info("***** EFTER ********");
        info(strFmt("Salg: %1 %2 %3 %4",salesLine.SalesId,salesLine.ItemId,salesLine.LineNumber,salesLine.name));
        info(strFmt("Indkøb: %1 %2 %3 %4",purchLine.purchid,purchLine.ItemId,purchLine.LineNumber,purchLine.name));
    }
    ttsCommit;
}


With this code at hand I could continue to customize the PurchAutoCreate_Sales class to sync the names of the salesLines to the purchLines.

Calculating easter sunday in different ways

$
0
0

Once a long time ago I found an interesting document on the inter web, describing different calendar systems.

From that I made a php-class for my website that can calculate easter sunday in the gregorian calendar. Then it is easy to calculate the rest of the danish holidays as they are offset according to easter sunday (except of course those that have a fixed date).

I translated that class in to x++.

You can find it here:

https://onedrive.live.com/redir?resid=9B63D38F981FFD1B!39710&authkey=!ABv0yYPULV3ReIE&ithint=file%2cxpo

Today I got talking with a colleague about calculating easter sunday so he mentioned that he had also made a version of the calculation:

http://stackoverflow.com/questions/11048524/how-to-calculate-easter-sunday-in-x

So of course - being a bit nerdy - we just *had* to check if the routines arrived at the same result. So we came up with:


staticvoid EasterTest(Args _args)
{
    Yr x;
    date easter(Yr yr) // Påskedag / påske søndag
    {
        int g,c,h,i,j,l;
        int easterday, eastermonth;
        g = yr mod19;
        // Gregorian Calendar
        c = real2int(rounddown(yr / 100,1));
        h = (c - roundDown(c/4,1)-rounddown(((8*c)+13)/25,1)+(19*g)+15) mod30;
        i = h - rounddown(h/28,1)*(1- rounddown(29 / (h+1),1) * rounddown((21-g) / 11,1));
        j = (yr + (rounddown(yr/4,1))+i+2 - c + (rounddown(c/4,1))) mod7;
        l = i - j;
        easterday = 3+rounddown((l+40)/44,1);
        returnmkdate(l+28-31*rounddown(easterday / 4,1), easterday, yr);
    }
    date dateOfEaster(Yr y)
    {
        int a = y mod19;
        int b = y div100;
        int c = y mod100;
        int d = b div4;
        int e = b mod4;
        int f = (b+8) div25;
        int g = (b-f+1) div3;
        int h = (19*a+b-d-g+15) mod30;
        int i = c div4;
        int k = c mod4;
        int l = (32+2*e+2*i-h-k) mod7;
        int m = (a+11*h+22*l) div451;
        int n = (h+l-7*m+114) div31;
        int p = (h+l-7*m+114) mod31;
        returnmkdate(p+1,n,y);
    }
    for (x = 1900; x <= 2154; x++)
        if (dateOfEaster(x) != easter(x))
            info(strFmt('E1=%1 E2=%2', dateOfEaster(x), easter(x)));
}

And the routines calculated the same dates for easter sunday.

We stopped ourselves when discussing if we should implement a tick-couting-measurement to see if my colleagues routine was faster than mine. :-)

But I think mine is slower as it uses several function calls and not pure arithmetic.
Viewing all 54 articles
Browse latest View live