This was my first AX 2012 SSRS customization and I think it was one of the harder ones. Here is everything I had to do to get this to work.
1) Add fields to purchLine table
2) Add fields to purchPurchaseOrderTmp table - this is used by the PurchPurchaseOrder report
3) Add fields to purchLineHistory table - this is used by the PurchLineArchivedVersions View and Query
4) Update PurchLineArchivedVersions view - drag the new field from the PurchLineHistory datasource into the Fields node of the view. Compile the view.
5) Update PurchLineNotArchivedVersions view - drag the new field from the PurchLine datasource into the Fields node of the view. Compile the view.
6) Compile the PurchLineAllVersions view and Synchronize. The field should be available in the PurchLineArchivedVersions and PurchLineNotArchivedVersions datasources on this view. Drag the new field from the PurchLineArchivedVersions view into the Fields node of the PurchLineAllVersions view. Compile this view.
7) Go to the PurchLineAllVersions QUERY now. You have to manually add the new field to the PurchLineArchivedVersions and PurchLineNotArchivedVersions datasources of this query because the dynamic fields property is set to No. Steps 3-7 are all being done because the PurchPurchaseOrderDP (data provider) class uses the PurchLineAllVersions view to copy data to the tmp table, so you need your new field there for step 8.
8) Modify the setPurchPurchaseOrderDetails() method on the PurchPurchaseOrderDP class and add code there to copy your field from PurchLineAllVersions to the PurchPurchaseOrderTmp table. It will look something like:
purchPurchaseOrderTmp.NewField = purchLineAllVersions.NewField;
Now you have to modify the report in visual studio. This was tricky. When I tried to refresh the dataset of the report to see my new fields I got some errors about a parameter on the report. It said Element:PurchPurchaseOrder.Parameters.IsPurchConfirmationRequestJournal has already been defined.
I ended up having to delete the Report parameter (isPurchConfirmationRequestJournal) on the report in Visual studio and refresh the datasets. Refreshing the datasets recreated the report parameter properly.
Hope this helps!
Dynamics AX solutions for issues I encounter as I develop for Dynamics AX - now Dynamics 365 Finance & Operations
Friday, May 4, 2018
Wednesday, July 8, 2015
Allowing the value of 0.00 on a form for a mandatory field that is variable type Real
As you know, the null value in the database for a real number is 0.0. If you have a form where you have this field marked mandatory and want to allow 0.0 this causes issues because the form reads the 0.0 in as null and gives you the error "Field 'X' must be filled in." You will receive this error before it even hits any of your validation code to determine if 0.0 is actually a valid value for that field.
Here is how I got around this. In the validate() method on the control (it has to be at the control level - if you also want it on the datasource, you can add it there too) I commented out the super() and just allowed it to return true (you can also call your own validateField() method here if you like). My field happened to have an edit method, so in the edit method I called validateField() and modifiedField() on the table. When modifiedField() returned, I checked the value of my field, if it returned from validate/modified successfully and it was still 0, then I set the showZero() property of the control on the form to true. This shows the user the 0 that they entered. Remember to set your control AutoDeclaration property to Yes so that you can use the field control in the code easily.
edit Field editField(boolean _set, Field _edtField)
{
;
if (_set)
{
Table.Field = _edtField;
if (Table.validateField(fieldNum(Table,Field)))
{
Table.modifiedField(fieldNum(Table, Field));
edtField = _edtField; // edtField is a global variable on the form
if(edtField == 0.0)
{
Field_Control.showZero(true);
}
}
}
return edtField;
}
Here is how I got around this. In the validate() method on the control (it has to be at the control level - if you also want it on the datasource, you can add it there too) I commented out the super() and just allowed it to return true (you can also call your own validateField() method here if you like). My field happened to have an edit method, so in the edit method I called validateField() and modifiedField() on the table. When modifiedField() returned, I checked the value of my field, if it returned from validate/modified successfully and it was still 0, then I set the showZero() property of the control on the form to true. This shows the user the 0 that they entered. Remember to set your control AutoDeclaration property to Yes so that you can use the field control in the code easily.
edit Field editField(boolean _set, Field _edtField)
{
;
if (_set)
{
Table.Field = _edtField;
if (Table.validateField(fieldNum(Table,Field)))
{
Table.modifiedField(fieldNum(Table, Field));
edtField = _edtField; // edtField is a global variable on the form
if(edtField == 0.0)
{
Field_Control.showZero(true);
}
}
}
return edtField;
}
Filter controls on a form with user initiated filters
I added filters to the standard work order form in Dynamics AX. They are checkboxes for what status the user would like to filter. However, if a user filtered their own fields using the filtering grid (CTRL-G) and then they checked or unchecked a status box, their user filters would disappear and the form would reset using the checkbox filters.
Here is how I fixed this:
Set the initial ProdStatus filter using the usage data of the user for the original checkboxes that will be used. Do this in the init() method of the form after getting the usage data and setting the controls.
xSysLastValue::getLast(this);
StatusCreated.value(bStatusCreated);
StatusCostEstimated.value(bStatusCostEstimated);
StatusScheduled.value(bStatusScheduled);
StatusReleased.value(bStatusReleased);
StatusStartedUp.value(bStatusStartedUp);
StatusReportedFinished.value(bStatusReportedFinished);
StatusCompleted.value(bStatusCompleted);
For each status, add it to the status string field you are building:
if (bStatusCreated)
{
if (!strLen(statusFilter))
{
statusFilter += int2str(enum2int(ProdStatus::Created));
}
else
{
statusFilter += ',' + int2str(enum2int(ProdStatus::Created));
}
} . . .
Then set your query range:
qbrStatus = this.query().dataSourceTable(tablenum(ProdTable)).addRange(fieldnum
(ProdTable, ProdStatus));
if (strLen(statusFilter))
{
qbrStatus.status(RangeStatus::Hidden);
qbrStatus.value(statusFilter);
}
else
{
qbrStatus.value(SysQuery::valueUnlimited());
}
In the modified method of the checkbox status controls, retrieve the prodTable_ds.queryRun().query() and then modify it with your current status checkbox. Then call ProdTable_DS.research(). Calling executeQuery() again was what was resetting the form to the original query. Using research() will keep all the user filters the same.
str statusFilter;
Query queryProdTable;
QueryBuildDatasource qbdsProdTable;
;
if (!ProdTable_DS.queryRun()) // just in case
{
return;
}
queryProdTable = ProdTable_DS.queryRun().query();
qbdsProdTable = queryProdTable.dataSourceTable(tableNum(ProdTable));
Just like in the init() method, for each status, add it to the statusFilter string and then set your status. This new qbrStatus is using the the query from queryRun() though so it will maintain user filters. Calling research() on the datasource will run this filtered query on your form.
qbrStatus = SysQuery::findOrCreateRange(qbdsProdTable, fieldNum(ProdTable, ProdStatus));
qbrStatus.status(RangeStatus::Hidden);
qbrStatus.value(statusFilter);
ProdTable_DS.research();
Here is how I fixed this:
Set the initial ProdStatus filter using the usage data of the user for the original checkboxes that will be used. Do this in the init() method of the form after getting the usage data and setting the controls.
xSysLastValue::getLast(this);
StatusCreated.value(bStatusCreated);
StatusCostEstimated.value(bStatusCostEstimated);
StatusScheduled.value(bStatusScheduled);
StatusReleased.value(bStatusReleased);
StatusStartedUp.value(bStatusStartedUp);
StatusReportedFinished.value(bStatusReportedFinished);
StatusCompleted.value(bStatusCompleted);
For each status, add it to the status string field you are building:
if (bStatusCreated)
{
if (!strLen(statusFilter))
{
statusFilter += int2str(enum2int(ProdStatus::Created));
}
else
{
statusFilter += ',' + int2str(enum2int(ProdStatus::Created));
}
} . . .
Then set your query range:
qbrStatus = this.query().dataSourceTable(tablenum(ProdTable)).addRange(fieldnum
(ProdTable, ProdStatus));
if (strLen(statusFilter))
{
qbrStatus.status(RangeStatus::Hidden);
qbrStatus.value(statusFilter);
}
else
{
qbrStatus.value(SysQuery::valueUnlimited());
}
In the modified method of the checkbox status controls, retrieve the prodTable_ds.queryRun().query() and then modify it with your current status checkbox. Then call ProdTable_DS.research(). Calling executeQuery() again was what was resetting the form to the original query. Using research() will keep all the user filters the same.
str statusFilter;
Query queryProdTable;
QueryBuildDatasource qbdsProdTable;
;
if (!ProdTable_DS.queryRun()) // just in case
{
return;
}
queryProdTable = ProdTable_DS.queryRun().query();
qbdsProdTable = queryProdTable.dataSourceTable(tableNum(ProdTable));
Just like in the init() method, for each status, add it to the statusFilter string and then set your status. This new qbrStatus is using the the query from queryRun() though so it will maintain user filters. Calling research() on the datasource will run this filtered query on your form.
qbrStatus = SysQuery::findOrCreateRange(qbdsProdTable, fieldNum(ProdTable, ProdStatus));
qbrStatus.status(RangeStatus::Hidden);
qbrStatus.value(statusFilter);
ProdTable_DS.research();
Thursday, September 6, 2012
Production order status reset (from code vs. form)
Recently I had the need to change the status of an order back to "Created" programmatically. I found lots of blogs about how to go forward with the status, but going back was hard to find. Here's what I ended up doing. I wrote a class with 3 methods. A classdeclaration() with a prodid variable (used below), a parmProdId method and the following method:
boolean resetStatusToCreated
{
ProdMultiStatusDecrease prodMultiStatusDecrease;
ProdParmStatusDecrease prodParmStatusDecrease;
ProdTable prodTable;
Args args = new Args();
;
select prodTable where prodTable.ProdId == prodID;
args.record(prodTable);
prodParmStatusDecrease.clear();
prodParmStatusDecrease.initFromProdTable(prodTable);
prodParmStatusDecrease.WantedStatus = ProdStatus::Created;
prodParmStatusDecrease.ParmId = NumberSeq::newGetNum(CompanyInfo::numRefParmId()).num();
prodParmStatusDecrease.insert();
prodMultiStatusDecrease = prodMultiStatusDecrease::construct(args);
prodMultiStatusDecrease.initParmBuffer(prodParmStatusDecrease);
prodMultiStatusDecrease.parmId(prodParmStatusDecrease.ParmId);
prodMultiStatusDecrease.run();
select prodTable where prodTable.ProdId == prodId;
if(prodTable.ProdStatus == prodStatus::Created)
{
return true;
}
return false;
It seems to have worked. I just have to verify that everything was reversed properly. Testing the code is as easy as doing this:
ResetProdStatusClass = new ResetClass();
ResetProdStatusClass.parmProdId('WO000001');
ResetProdStatusClass.resetStatusToCreated();
I specifically needed to take it back to the Created status, but I imagine you could modify this to pass in the status you wanted it reset to and set it in the WantedStatus field.
Good luck!
boolean resetStatusToCreated
{
ProdMultiStatusDecrease prodMultiStatusDecrease;
ProdParmStatusDecrease prodParmStatusDecrease;
ProdTable prodTable;
Args args = new Args();
;
select prodTable where prodTable.ProdId == prodID;
args.record(prodTable);
prodParmStatusDecrease.clear();
prodParmStatusDecrease.initFromProdTable(prodTable);
prodParmStatusDecrease.WantedStatus = ProdStatus::Created;
prodParmStatusDecrease.ParmId = NumberSeq::newGetNum(CompanyInfo::numRefParmId()).num();
prodParmStatusDecrease.insert();
prodMultiStatusDecrease = prodMultiStatusDecrease::construct(args);
prodMultiStatusDecrease.initParmBuffer(prodParmStatusDecrease);
prodMultiStatusDecrease.parmId(prodParmStatusDecrease.ParmId);
prodMultiStatusDecrease.run();
select prodTable where prodTable.ProdId == prodId;
if(prodTable.ProdStatus == prodStatus::Created)
{
return true;
}
return false;
It seems to have worked. I just have to verify that everything was reversed properly. Testing the code is as easy as doing this:
ResetProdStatusClass = new ResetClass();
ResetProdStatusClass.parmProdId('WO000001');
ResetProdStatusClass.resetStatusToCreated();
I specifically needed to take it back to the Created status, but I imagine you could modify this to pass in the status you wanted it reset to and set it in the WantedStatus field.
Good luck!
Wednesday, August 15, 2012
Using containers vs. other collection classes
I’ve
seen containers used on some forms, so I thought this would be useful
information.
This is based on my experience and some sites I’ve read (which I’ve given links to below).
This is based on my experience and some sites I’ve read (which I’ve given links to below).
If
you’re going to store recIds as your datatype in any of these collection
classes and you're not on AX 2012 yet, you should
define the type like this (because AX 2012 handles recIds differently, it
will be easier
for upgrading when you do this):
Declare
a dictype variable like this:
DictType dt = new DictType(extendedTypeNum(recid));
DictType dt = new DictType(extendedTypeNum(recid));
Then
use it like this, here are examples for a map and a set:
msgMap = new Map(dt.baseType(),Types::String);
msgMap = new Map(dt.baseType(),Types::String);
setRecIds
= new Set(dt.baseType());
Containers:
Containers are dynamic and have no limits. They can contain elements of almost all data types: boolean, integer, real, date, string, container, arrays, tables, and extended data types. However, objects may not be stored in containers.Containers in AX are used very often. It’s easy to work with them, but
data in containers are stored sequentially, and retrieved sequentially. This means that containers provide slower data access if you are working with a large numbers of records. You cannot modify a container in-place, instead each addition or deletion has to iterate over the entire structure to copy all values into a newly allocated one. So every container manipulation has a run time of O(n). This is why they are not recommended to be used on forms. This site has some good information about how to use containers more efficiently when you do need to use them http://www.axaptapedia.com/index.php?title=Container If you are storing unique data of one type (like storing recids for a checkbox on a form),you can use a Set (see below). If you are storing non-unique data of the same type, use a List (see below).
Maps are most useful if you need a key for your data. I used a map recently to store the recId as a key to save/retrieve the inventTransId of some records. This is a good explanation of how to use Maps (http://www.axaptapedia.com/index.php?title=Map_Class)
Sets:
Sets are an unordered list of items. If you try to add something to a set that is already in the set, it will ignore it. Sets have an in() and remove() method which is useful. This is a good explanation of how to use Sets http://www.axaptapedia.com/index.php?title=Set_Class
Lists:
Lists contain elements that are accessed sequentially. Lists provide getEnumerator() and getIterator() methods (like sets do) which allow you to insert and delete items from the list. Here’s some information about Lists http://msdn.microsoft.com/en-us/library/aa848905%28v=ax.10%29.aspx.
Monday, April 30, 2012
Using the Fill Utility on the table and/or fields you specify
We have a customized field in AX that we want the user to be able to select multiple records and change them all to the same value at once. Dynamics AX 2009 has a fill utility that can do this. However, the fill utility can only be used on tables that are "main" tables (property TableGroup = main). I wanted it to be able to be used on my custom field (a date field) on the SalesTable form. Here's what I had to do:
On the Form: SysRecordInfo, Method: init
Add code to allow the fill utility to be used on my table.
void init()
{
....
// don't do the field level testing if the table doesn't meet the base requirement
if (dictTable.tableGroup() == TableGroup::Main ||
common.TableId == tablenum(LedgerJournalTrans)
//N - GW_Admin_FillUtility - Arains 03/02/2012
// Allow the fill utility to be used on the SalesTable
|| common.TableId == tablenum(SalesTable))
{
element.fillUtilityInit();
}
else
{
fillGrp.visible(false);
}
....
}
Also, on the Method: fillUtilityInit
Add code to allow the fillUtility to be used for my custom field on the salestable and only my custom field on the SalesTable.
void fillUtilityInit()
{
....
if (fieldIdLocal == dictTableLocal.primaryKeyField() ||
fieldIdLocal == dictTableLocal.fieldName2Id('RECID') ||
fieldIdLocal == dictTableLocal.fieldName2Id('DATAAREAID'))
{
fillGrp.visible(false);
return;
}
// New code for fill utility modifications
// If this is the salesTable and this is the custom date field
// allow the fill utility to be used, disallows for all other fields
if(tblId == TableNum(SalesTable) &&
fieldIDLocal != dictTableLocal.fieldName2Id('YourCustomField'))
{
fillGrp.visible(false);
return;
}
....
}
On the Form: SysRecordInfo, Method: init
Add code to allow the fill utility to be used on my table.
void init()
{
....
// don't do the field level testing if the table doesn't meet the base requirement
if (dictTable.tableGroup() == TableGroup::Main ||
common.TableId == tablenum(LedgerJournalTrans)
//N - GW_Admin_FillUtility - Arains 03/02/2012
// Allow the fill utility to be used on the SalesTable
|| common.TableId == tablenum(SalesTable))
{
element.fillUtilityInit();
}
else
{
fillGrp.visible(false);
}
....
}
Also, on the Method: fillUtilityInit
Add code to allow the fillUtility to be used for my custom field on the salestable and only my custom field on the SalesTable.
void fillUtilityInit()
{
....
if (fieldIdLocal == dictTableLocal.primaryKeyField() ||
fieldIdLocal == dictTableLocal.fieldName2Id('RECID') ||
fieldIdLocal == dictTableLocal.fieldName2Id('DATAAREAID'))
{
fillGrp.visible(false);
return;
}
// New code for fill utility modifications
// If this is the salesTable and this is the custom date field
// allow the fill utility to be used, disallows for all other fields
if(tblId == TableNum(SalesTable) &&
fieldIDLocal != dictTableLocal.fieldName2Id('YourCustomField'))
{
fillGrp.visible(false);
return;
}
....
}
Friday, March 30, 2012
Unpicking the entire order all at once
For orders in AX (I am addressing sales orders specifically here), if you want to unpick an order, you have to go to the sales line and, one-by-one, unpick the order. You click on the Inventory button and select the Pick option. In the form, check the autocreate box, then click Post All in the lower area of the form. If you regularly have to pick and unpick orders, especially if you have a lot of lines on your orders, this can become very tedious.
I wrote a job that will unpick the entire order. Now I'm going to put an Unpick button on the sales order form at the order level and allow certain users (security will be used) to do this. I will most likely allow multiple orders to be selected so you can unpick multiple orders with one click of a button. The code for the job I wrote is below:
// For testing, I set the salesid here.
// In the final code, I will have to pass in the salesTable record
// from the salesTable_ds of the form
SalesId salesid = 'RSO948671';
TmpInventTransWMS tmpinventTransWMS;
InventMovement movement;
InventTrans inventTrans;
salesline salesline;
inventtransWMS_pick inventTransPick;
;
while select salesline
where salesline.SalesId == salesId
{
select inventTrans
where inventTrans.TransRefId == salesline.SalesId &&
inventTrans.ItemId == salesline.ItemId &&
inventTrans.StatusIssue == StatusIssue::Picked;
if(inventTrans.RecId)
{
movement = null;
movement = InventMovement::construct(salesLine);
inventTranspick = new InventTransWMS_Pick(movement,tmpInventTransWMS);
tmpInventTransWMS = null;
tmpInventTransWMS.initFromInventTrans(inventTrans);
tmpInventTransWMS.InventQty = inventTrans.StatusIssue == StatusIssue::Picked ? inventTrans.Qty : -inventTrans.Qty;
tmpInventTransWMS.insert();
inventTransWMS_pick::updateInvent(inventTransPick, tmpInventTransWMS);
}
}
I wrote a job that will unpick the entire order. Now I'm going to put an Unpick button on the sales order form at the order level and allow certain users (security will be used) to do this. I will most likely allow multiple orders to be selected so you can unpick multiple orders with one click of a button. The code for the job I wrote is below:
// For testing, I set the salesid here.
// In the final code, I will have to pass in the salesTable record
// from the salesTable_ds of the form
SalesId salesid = 'RSO948671';
TmpInventTransWMS tmpinventTransWMS;
InventMovement movement;
InventTrans inventTrans;
salesline salesline;
inventtransWMS_pick inventTransPick;
;
while select salesline
where salesline.SalesId == salesId
{
select inventTrans
where inventTrans.TransRefId == salesline.SalesId &&
inventTrans.ItemId == salesline.ItemId &&
inventTrans.StatusIssue == StatusIssue::Picked;
if(inventTrans.RecId)
{
movement = null;
movement = InventMovement::construct(salesLine);
inventTranspick = new InventTransWMS_Pick(movement,tmpInventTransWMS);
tmpInventTransWMS = null;
tmpInventTransWMS.initFromInventTrans(inventTrans);
tmpInventTransWMS.InventQty = inventTrans.StatusIssue == StatusIssue::Picked ? inventTrans.Qty : -inventTrans.Qty;
tmpInventTransWMS.insert();
inventTransWMS_pick::updateInvent(inventTransPick, tmpInventTransWMS);
}
}
Wednesday, January 11, 2012
Go to main table form dynamic use
You can set the FormRef property on a table to a display menu item that references a class. This will allow you to open different forms depending on some field value in the record that is passed. In the class main() you can retrieve the record and then open the appropriate form from code.
Tuesday, October 25, 2011
How to unhide grid views
We all know how you can right-click on the grid of a form and use MorphX to add fields, remove fields, etc. You can also save and load views of a grid. Have you ever accidentally right-clicked on a form and clicked "hide" and not been able to figure out how to unhide?
In the example below, I have clicked "Load" to see all the views I could load for this grid. I then right-clicked on my list and said "Hide."
Now that I've hidden it, I cannot right-click and "unhide." I've lost my list of views. The only way to fix it is to remove a record from your usage data.
Go to Tools > Options to modify your own usage data or go to Admin > Users > select the user that is having this issue, click on the "User options" button. From there, click the "Usage data" button. Go to the last tab that says "All usage data." Highlight the row that has Record Type = UserSetup and Name = SysPick (see below) and delete that record (hit the big red X at the top of the form or press Alt-F9).
In the example below, I have clicked "Load" to see all the views I could load for this grid. I then right-clicked on my list and said "Hide."
Now that I've hidden it, I cannot right-click and "unhide." I've lost my list of views. The only way to fix it is to remove a record from your usage data.
Go to Tools > Options to modify your own usage data or go to Admin > Users > select the user that is having this issue, click on the "User options" button. From there, click the "Usage data" button. Go to the last tab that says "All usage data." Highlight the row that has Record Type = UserSetup and Name = SysPick (see below) and delete that record (hit the big red X at the top of the form or press Alt-F9).
Friday, October 21, 2011
Calling AIF Web Service - not sending all the fields into AX
We had the occasion to call the Dynamics AX SalesOrder create web service. However, we were trying to pass in the SalesOrder.SalesType field and it wouldn't come through in the XML that AIF read. The element and the attribute were not there in the XML file. We spoke with someone at Microsoft and they told us that you had to use the "specified" property on the field. For example (this is C#):
salesOrder.SalesType = AxdEnum_SalesType.Sales;
salesOrder.SalesTypeSpecified = true;
This seemed to do the trick!
salesOrder.SalesType = AxdEnum_SalesType.Sales;
salesOrder.SalesTypeSpecified = true;
This seemed to do the trick!
Subscribe to:
Posts (Atom)
