We're looking into refining our User Groups in Dynamics AX 2009 into more precise and fine-tuned groupings due to the wide range of variability between specific people within the same department. With this plan, it wouldn't be uncommon for majority of our users to fall user 5+ user groups.
Part of this would involve us expanding the default length of the User Group ID from 10 to 40 (as per Best Practice for naming conventions) since 10 characters don't give us enough room to adequately name each group as we would like (again, based on Best Practice Naming Conventions).
We have found that the main information seems to be obtained from the UserGroupInfo table, but that table isn't present under the Data Dictionary (it's under the System Documentation, so unavailable to be changed that way by my understanding). We've also found the UserGroupName EDT, but that is already set at 40 characters. The form itself doesn't seem to restricting the length of the field either. We've discussed changing the field on the SQL directly, but again my understanding is that if we do a full synchronization it would overwrite this change.
Where can we go to change this particular setting, or is it possible to change?
The size of the user group id is defined as as system extended data type (here \System Documentation\Types\userGroupId) and you cannot change any of the properties including the size 10 length.
You should live with that, don't try to fake the system using direct SQL changes. Even if you did that, AX would still believe that length is 10.
You could change the SysUserInfo form to show the group name only. The groupId might as well be assigned by a number sequence in your context.
I wrote a job to change the string size via X++ and it works for EDTs, but it can't seem to find the "userGroupId". From the general feel of AX I get, I'd be willing to guess that they just have it in a different location, but maybe not. I wonder if this could be tweaked to work:
static void Job9(Args _args)
{
#AOT
TreeNode treeNode;
Struct propertiesExt;
Map mapNewPropertyValues;
void setTreeNodePropertyExt(
Struct _propertiesExt,
Map _newProperties
)
{
Counter propertiesCount;
Array propertyInfoArray;
Struct propertyInfo;
str propertyValue;
int i;
;
_newProperties.insert('IsDefault', '0');
propertiesCount = _propertiesExt.value('Entries');
propertyInfoArray = _propertiesExt.value('PropertyInfo');
for (i = 1; i <= propertiesCount; i++)
{
propertyInfo = propertyInfoArray.value(i);
if (_newProperties.exists(propertyInfo.value('Name')))
{
propertyValue = _newProperties.lookup(propertyInfo.value('Name'));
propertyInfo.value('Value', propertyValue);
}
}
}
;
treeNode = TreeNode::findNode(#ExtendedDataTypesPath);
// This doesn't seem to be able to find the system type
//treeNode = treeNode.AOTfindChild('userGroupId');
treeNode = treeNode.AOTfindChild('AccountCategory');
propertiesExt = treeNode.AOTgetPropertiesExt();
mapNewPropertyValues = new Map(Types::String, Types::String);
mapNewPropertyValues.insert('StringSize', '30');
setTreeNodePropertyExt(propertiesExt, mapNewPropertyValues);
treeNode.AOTsetPropertiesExt(propertiesExt);
treeNode.AOTsave();
info("Done");
}
Related
I am trying to import a transfer order line with this code from Transfer Orders Import:
InventDim inventDim;
InventTransferLine inventTransferLine;
#define.ShipDate("1/1/2016")
#define.ReceiveDate("1/1/2016")
//Order line
inventDim.clear();
inventDim.InventSiteId = "GENERAL";
inventDim.InventLocationId = "103";
inventTransferLine.clear();
inventTransferLine.initValue();
inventTransferLine.ItemId = "A01103472";
inventTransferLine.InventDimId = InventDim::findOrCreate(inventDim).inventDimId;
inventTransferLine.QtyTransfer = 2;
inventTransferLine.initFromInventTableModule(InventTableModule::find(inventTransferLine.ItemId,ModuleInventPurchSales::Invent));
inventTransferLine.QtyRemainReceive = inventTransferLine.QtyTransfer;
inventTransferLine.QtyRemainShip = inventTransferLine.QtyTransfer;
inventTransferLine.ShipDate = str2Date(#ShipDate, 213);
inventTransferLine.ReceiveDate = str2Date(#ReceiveDate, 213);
inventTransferLine.initFromInventTransferTable(inventTransferTable, false);
inventTransferLine.LineNum = InventTransferLine::lastLineNum(inventTransferLine.TransferId) + 1.0;
if (inventTransferLine.validateWrite())
{
inventTransferLine.insert();
}
else
throw error("Order line");
Is this the correct or the preferred way to do it?
What's the use of inventDim here? I am transferring this product from warehouse A to warehouse B and those are specified in the selected header, meaning the InventTransferTable record.
And i am not sure about these two lines:
1. inventTransferLine.QtyRemainReceive = inventTransferLine.QtyTransfer;
2. inventTransferLine.QtyRemainShip = inventTransferLine.QtyTransfer;
RemainReceive from where? i can't figure out what are they referring to.
You are more or less good to go.
You seems to have copied what others have done, which is good.
There are other ways to it, one using AxInventTransferTable and ...Line classes, another using the TransferOrderCreateService service. None will give you much competitive advantage, if working from within AX.
The InventDim (see white paper) contains the inventory, storage, and tracking dimensions of the item. You will need to set more fields if the item requires it as specified on the item and product.
Product dimensions. Item dimensions, color, size and configuration.
Storage dimensions. These are Site, Warehouse, Location, and Pallet.
Tracking dimensions. These are Batch number and Serial number.
A shipment is a two-step thing. First you ship the item from the source site/warehouse. Later you receive the item at the target site/warehouse.
The QtyRemainShipand QtyRemainReceive fields represents the quantity remaining for each step.
I'm currently having troubles figuring out how to use Java 8 streams.
I'm trying to go from lista_dottori (Map<Integer, Doctor>) to a new map patientsPerSp where to every medical specialization (method getSpecialization) I map the number of patients wich have a doctor with this specialization (method getPatients in class Doctor returns a List of that doctor's patients). I can't understand how to use the counting method for this purpose, and I can't seem to find any examples nor explanations for this kind of problems on the internet.
That's what i've written, it does give me error in the counting section:
public Collection<String> countPatientsPerSpecialization(){
patientsPerSp=
lista_dottori.values().stream()
.map(Doctor::getSpecialization)
.collect(groupingBy(Doctor::getSpecialization, counting(Doctor::getPatients.size())))
;
}
Seems that you want to sum the sizes of patients lists. This can be done by summingInt() collector, not counting() (which just counts occurences; doctors in this case). Also mapping seems to be unnecessary here. So you cuold write:
patientsPerSp = lista_dottori.values().stream()
.collect(groupingBy(Doctor::getSpecialization,
summingInt(doctor -> doctor.getPatients().size())));
Note that the results will be incorrect if several doctors have the same patient (this way it will be counted several times). If it's possible in your case, then it would be probably better to make a set of patients:
patientsPerSp = lista_dottori.values().stream()
.collect(groupingBy(Doctor::getSpecialization,
mapping(Doctor::getPatients(), toSet())));
This way you will have a map which maps specialization to the set of patients, so the size of this set will be the count which you want. If you just need the count without set, you can add a final step via collectingAndThen():
patientsPerSp = lista_dottori.values().stream()
.collect(groupingBy(Doctor::getSpecialization,
collectingAndThen(
mapping(Doctor::getPatients(), toSet()),
Set::size)));
I solved the problem avoiding using the streams. That's the solution I used:
public Collection<String> countPatientsPerSpecialization(){
int numSpec = 0;
Map<String, Integer> spec = new HashMap<>();
for(Doctor d : lista_dottori.values()){
if(!spec.containsKey(d.getSpecialization())){
spec.put(d.getSpecialization(), d.getPatients().size());
numSpec++;
}
else{ //cioè se la specializzazione c'è già
spec.replace(d.getSpecialization(), +d.getPatients().size());
}
}
patientsPerSp.add(spec.keySet() + ": " + spec.values());
for(String s : patientsPerSp)
System.out.println(s);
return patientsPerSp;
}
I couldn't seem to be able to solve it using your solutions, although they were very well exposed, sorry.
Thank you anyway for taking the time to answer
Map<String, Integer> patientsPerSpecialization =
doctors.values()
.stream()
.collect(Collectors.groupingBy(Doctor::getSpecialization,
Collectors.summingInt(Doctor::nberOfAssignedPatients)));
Anyone have sample lookup code for AOT objects? (or know where to find the one they use for the AX properties window)
I need to replicate the functionality that you see in several fields in the properties window. The ExtendedDataType field is a good example. Type a few letters, hit the down arrow, and a filtered list of AOT ExtendedDataType objects appears.
I've been trying to use treeNode findChildren to build my custom lookup list, but it is very slow. Whatever method AX is using happens instantly.
Thanks
Try this:
Dictionary d = new Dictionary();
int i;
int cnt = d.tableCnt();
TableId tableId;
str nameForLookup;
for (i = 1; i <= cnt; i++)
{
tableId = d.tablecnt2id(i);
nameForLookup = tableid2name(tableId);
}
Queries to the Model/Util*Element tables will not be cached, and are relatively slow due to the number of records that they contain.
There may be other factors slowing down execution as well. If you are on 2012, for lookups, you may want to build a temp table with an XDS() method, which populates itself using the above code, then you can simply select from that table (and it will be cached for the session):
create a SQL Temp table (e.g. with a name like MyTableLookup), add a name column
add a method like this:
public RefreshFrequency XDS()
{
MyTableLookup tableLookup;
ttsbegin;
// Use the above code to insert records into tableLookup
ttscommit;
return RefreshFrequency::PerSession;
}
bind your form to MyLookupTable
You may develop an estándar EDT linked to the UtilElement Table properly filtered. This will show a list of objects and will have same functionality of all table-linked text fields.
Is there an example anywhere of a form that performs running totals in a column located within a grid. The user ordering and filtering of the grid would affect the running totals column.
I can easily perform the above if it was ordering only by transaction date, but including the user ordering and filtering I presume that we would have to use the datasource range() and rangecount() functions (see SysQuery::mergeRanges() for an example) then iterate over these to apply the filtering, then include the dynalinks. The same for the ordering, albeit this is now more complicated.
Any suggestions appreciated. Any appreciations suggested (as in: vote the question up!).
You could implement it as a form datasource display method using this strategy:
Copy the form's datasource query (no need for SysQuery::mergeRanges):
QueryRun qr = new QueryRun(ledgerTrans_qr.query());
Iterate and sum over your records using qr, stop after the current record:
while (qr.next())
{
lt = qr.getNo(1);
total += lt.AmountMST;
if (lt.RecId == _lt.RecId)
break;
}
This could be made more performant if the sorting order was fixed (using sum(AmountMST) and adding a where constraint).
Return the total
This is of cause very inefficient (subquadratic time, O(n^2)).
Caching the results (in a map) may make it usable if there are not too many records.
Update: a working example.
Any observations or criticisms to the code below most welcome. Jan's observation about the method being slow is still valid. As you can see, it's a modification of his original answer.
//BP Deviation Documented
display AmountMST XXX_runningBalanceMST(LedgerTrans _trans)
{
LedgerTrans localLedgerTrans;
AmountMST amountMST;
;
localLedgerTrans = this.getFirst();
while (localLedgerTrans)
{
amountMST += localLedgerTrans.AmountMST;
if (localLedgerTrans.RecId == _trans.RecId)
{
break;
}
localLedgerTrans = this.getNext();
}
return amountMST;
}
We'll soon be embarking on the development of a new mobile application. This particular app will be used for heavy searching of text based fields. Any suggestions from the group at large for what sort of database engine is best suited to allowing these types of searches on a mobile platform?
Specifics include Windows Mobile 6 and we'll be using the .Net CF. Also some of the text based fields will be anywhere between 35 and 500 characters. The device will operate in two different methods, batch and WiFi. Of course for WiFi we can just submit requests to a full blown DB engine and just fetch results back. This question centres around the "batch" version which will house a database loaded with information on the devices flash/removable storage card.
At any rate, I know SQLCE has some basic indexing but you don't get into the real fancy "full text" style indexes until you've got the full blown version which of course isn't available on a mobile platform.
An example of what the data would look like:
"apron carpenter adjustable leather container pocket waist hardware belt" etc. etc.
I haven't gotten into the evaluation of any other specific options yet as I figure I'd leverage the experience of this group in order to first point me down some specific avenues.
Any suggestions/tips?
Just recently I had the same issue. Here is what I did:
I created a class to hold just an id and the text for each object (in my case I called it a sku (item number) and a description). This creates a smaller object that uses less memory since it is only used for searching. I'll still grab the full-blown objects from the database after I find matches.
public class SmallItem
{
private int _sku;
public int Sku
{
get { return _sku; }
set { _sku = value; }
}
// Size of max description size + 1 for null terminator.
private char[] _description = new char[36];
public char[] Description
{
get { return _description; }
set { _description = value; }
}
public SmallItem()
{
}
}
After this class is created, you can then create an array (I actually used a List in my case) of these objects and use it for searching throughout your application. The initialization of this list takes a bit of time, but you only need to worry about this at start up. Basically just run a query on your database and grab the data you need to create this list.
Once you have a list, you can quickly go through it searching for any words you want. Since it's a contains, it must also find words within words (e.g. drill would return drill, drillbit, drills etc.). To do this, we wrote a home-grown, unmanaged c# contains function. It takes in a string array of words (so you can search for more than one word... we use it for "AND" searches... the description must contain all words passed in... "OR" is not currently supported in this example). As it searches through the list of words it builds a list of IDs, which are then passed back to the calling function. Once you have a list of IDs, you can easily run a fast query in your database to return the full-blown objects based on a fast indexed ID number. I should mention that we also limit the maximum number of results returned. This could be taken out. It's just handy if someone types in something like "e" as their search term. That's going to return a lot of results.
Here's the example of custom Contains function:
public static int[] Contains(string[] descriptionTerms, int maxResults, List<SmallItem> itemList)
{
// Don't allow more than the maximum allowable results constant.
int[] matchingSkus = new int[maxResults];
// Indexes and counters.
int matchNumber = 0;
int currentWord = 0;
int totalWords = descriptionTerms.Count() - 1; // - 1 because it will be used with 0 based array indexes
bool matchedWord;
try
{
/* Character array of character arrays. Each array is a word we want to match.
* We need the + 1 because totalWords had - 1 (We are setting a size/length here,
* so it is not 0 based... we used - 1 on totalWords because it is used for 0
* based index referencing.)
* */
char[][] allWordsToMatch = new char[totalWords + 1][];
// Character array to hold the current word to match.
char[] wordToMatch = new char[36]; // Max allowable word size + null terminator... I just picked 36 to be consistent with max description size.
// Loop through the original string array or words to match and create the character arrays.
for (currentWord = 0; currentWord <= totalWords; currentWord++)
{
char[] desc = new char[descriptionTerms[currentWord].Length + 1];
Array.Copy(descriptionTerms[currentWord].ToUpper().ToCharArray(), desc, descriptionTerms[currentWord].Length);
allWordsToMatch[currentWord] = desc;
}
// Offsets for description and filter(word to match) pointers.
int descriptionOffset = 0, filterOffset = 0;
// Loop through the list of items trying to find matching words.
foreach (SmallItem i in itemList)
{
// If we have reached our maximum allowable matches, we should stop searching and just return the results.
if (matchNumber == maxResults)
break;
// Loop through the "words to match" filter list.
for (currentWord = 0; currentWord <= totalWords; currentWord++)
{
// Reset our match flag and current word to match.
matchedWord = false;
wordToMatch = allWordsToMatch[currentWord];
// Delving into unmanaged code for SCREAMING performance ;)
unsafe
{
// Pointer to the description of the current item on the list (starting at first char).
fixed (char* pdesc = &i.Description[0])
{
// Pointer to the current word we are trying to match (starting at first char).
fixed (char* pfilter = &wordToMatch[0])
{
// Reset the description offset.
descriptionOffset = 0;
// Continue our search on the current word until we hit a null terminator for the char array.
while (*(pdesc + descriptionOffset) != '\0')
{
// We've matched the first character of the word we're trying to match.
if (*(pdesc + descriptionOffset) == *pfilter)
{
// Reset the filter offset.
filterOffset = 0;
/* Keep moving the offsets together while we have consecutive character matches. Once we hit a non-match
* or a null terminator, we need to jump out of this loop.
* */
while (*(pfilter + filterOffset) != '\0' && *(pfilter + filterOffset) == *(pdesc + descriptionOffset))
{
// Increase the offsets together to the next character.
++filterOffset;
++descriptionOffset;
}
// We hit matches all the way to the null terminator. The entire word was a match.
if (*(pfilter + filterOffset) == '\0')
{
// If our current word matched is the last word on the match list, we have matched all words.
if (currentWord == totalWords)
{
// Add the sku as a match.
matchingSkus[matchNumber] = i.Sku.ToString();
matchNumber++;
/* Break out of this item description. We have matched all needed words and can move to
* the next item.
* */
break;
}
/* We've matched a word, but still have more words left in our list of words to match.
* Set our match flag to true, which will mean we continue continue to search for the
* next word on the list.
* */
matchedWord = true;
}
}
// No match on the current character. Move to next one.
descriptionOffset++;
}
/* The current word had no match, so no sense in looking for the rest of the words. Break to the
* next item description.
* */
if (!matchedWord)
break;
}
}
}
}
};
// We have our list of matching skus. We'll resize the array and pass it back.
Array.Resize(ref matchingSkus, matchNumber);
return matchingSkus;
}
catch (Exception ex)
{
// Handle the exception
}
}
Once you have the list of matching skus, you can iterate through the array and build a query command that only returns the matching skus.
For an idea of performance, here's what we have found (doing the following steps):
Search ~171,000 items
Create list of all matching items
Query the database, returning only the matching items
Build full-blown items (similar to SmallItem class, but a lot more fields)
Populate a datagrid with the full-blow item objects.
On our mobile units, the entire process takes 2-4 seconds (takes 2 if we hit our match limit before we have searched all items... takes 4 seconds if we have to scan every item).
I've also tried doing this without unmanaged code and using String.IndexOf (and tried String.Contains... had same performance as IndexOf as it should). That way was much slower... about 25 seconds.
I've also tried using a StreamReader and a file containing lines of [Sku Number]|[Description]. The code was similar to the unmanaged code example. This way took about 15 seconds for an entire scan. Not too bad for speed, but not great. The file and StreamReader method has one advantage over the way I showed you though. The file can be created ahead of time. The way I showed you requires the memory and the initial time to load the List when the application starts up. For our 171,000 items, this takes about 2 minutes. If you can afford to wait for that initial load each time the app starts up (which can be done on a separate thread of course), then searching this way is the fastest way (that I've found at least).
Hope that helps.
PS - Thanks to Dolch for helping with some of the unmanaged code.
You could try Lucene.Net. I'm not sure how well it's suited to mobile devices, but it is billed as a "high-performance, full-featured text search engine library".
http://incubator.apache.org/lucene.net/
http://lucene.apache.org/java/docs/