Showing posts with label Dynamics AX. Show all posts
Showing posts with label Dynamics AX. Show all posts

Tuesday, May 18, 2021

Dynamics AX SSRS reports using SubReport

This can be tricky for sure. The most important point to be understood is that there has to be a parameter from the Main report that can be passed to the Subreport in the subreport Parameters.

From the main report design, you Right-click > Insert > Subreport.

Then you Right-click the Subreport to view Subreport Properties.

On the General tab, you must set the report that you want to use as the subreport. This should be in the format: Report.Design.

Here is an example:



The parameters tab is where the real magic happens.

First of all, you need three parameters that are standard for all (most?) ssrs reports. The "Name" column on this parameters screen should be thought of as the parameters from the Sub report. The "Value" column in this screen should be thought of as the parameters from the Main. The value parameters can be an actual parameter field or just a data field from your report. The Name will be the name of the parameter from the sub report. The Name drop-down will not be helpful to you. You will need to type in the name of the parameter to pass. The Value drop-down will also not be helpful to you. You can use the "fx" to build the expression. If you do this, you can choose the "Parameters" category and double click the parameter you want in the Values section:

Or you can choose a field from your dataset as the parameter for the report.

Or you can type them in the Values section of the Subreport properties screen like this:

Name                                        Value

AX_CompanyName                [@AX_CompanyName]
AX_UserContext                     [@AX_UserContext]
AX_ReportContext                 [@AX_ReportContext]

Then you need to pass the parameter(s) that the report needs for it to run. My report was passing the AccountNum. The customer account number in the main report had parameter "CustListReportDS_AccountNum." The customer account number in the subreport had parameter [@DataSet1_Customer_account], so my parameter looked like:

Name                                                            Value
CustListReportDS_AccountNum             [@DataSet1_Customer_account]

It will look like this when you are done:



Hope this helps!




Friday, March 26, 2021

Changing a string field to match number sequence format

 Our customer is automating their invoice processing with a 3rd party program (AxTension). The invoices they receive from vendors often have only part of the purchase order number in them. Their current PO format is PO#######. Sometimes they just receive the numeric part, '1234567', sometimes the numeric part is missing the leading zeroes, '12345'. This kicks the PO numbers out as errors and the users would have to manually match them to the PO in the system.

I was asked to write logic that will "fix" the PO numbers that come in. First of all, if the PO number is blank, I just want to return (this avoids adding a po number for NON PO invoices):

if(_purchId == strMin())
{
    return _purchId;
}


Then I had to find the number sequence format for the PurchId extended data type.

NumberSequenceReference Ref;
NumberSequenceDataType dataType;
NumberSequenceTable    numSeq;
extendedTypeId                poTypeId;

//Find the EDT id for your extended data type (mine is PurchId)
poTypeId = extendedTypeNum(PurchId);

select firstOnly RecId from dataType
            where dataType.DatatypeId == poTypeId
            join numbersequenceid from Ref
            where Ref.numbersequencedatatype == datatype.recid|
            join Format from numSeq
            where numSeq.recid == Ref.numbersequenceid;

Once I have the numberSequenceTable record and the format field, I can use it to check the purchId that was passed into the system. I also did not want to show the user any errors so I store the length of the info log before and clear it after calling in to the numCheckFormat() method run a check on my format.

When you incorporate this into your class(es), you can have the user pass in the purchId and return the newPurchId you have fixed (public static PurchId validatePurchId(_purchId)). When you run it as a job, you can create the PurchId purchId variable and set it to the purchId you want to "fix."

int lines;
purchid purchid, newpurchid;

//Save current infolog spot
lines = infolog.line();   

// Check to see if purchid matches format
if(!NumberSeq::numCheckFormat(purchID,numSeq))
{  // if check fails, fix purchid
    //Cut any infolog messages that were created
    infolog.cut(lines ? lines : 1);
    //loop through the format
    newpurchid = purchid;

    for(i=1; i <= strLen(numseq.format); i++)
    {
        if(i > strLen(_purchId))
        { // if the purchid is longer than the format, we don't need to modify it (this only adds)
            break;
        }

        f = subStr(numSeq.format,i,1);
        p = substr(purchId,i,1);
        if (f != '#')
        { // if we aren't looking for a number in the format
            if(f != p)
            { // insert the character if it is in the format but not the purchid
                
if(strLen(purchId) < strLen(numSeq.Format))
                {
                        //insert character into string
                        purchId = strIns(purchId, f, i);
                }

                //fix issue where readsoft reads the letter O as a numeric 0 
                else if (p == '0')
               { //Overwrite character in the string
                        purchId = strPoke(purchId, f, i);
                }
            }  // END if f!=p     
        } // END if f!= '#'
    } // END for

    // After inserting the characters that exist in the format but not in the purchid, check the length
    // if it's not long enough, we want to insert 0s before the existing numbers in the purchid
    if(strLen(newpurchid) < strLen(numSeq.format))
    {
        zeroPos = strNFind(numSeq.Format,'#',strLen(numSeq.Format),-strLen(numSeq.Format));
        newPurchId = strIns(newpurchid,strRep('0',strLen(numSeq.format)-strLen(newpurchId)),zeroPos+1);
    }

You could also add logic that deletes unwanted characters from the purchid if it's not in the format, but we were assuming that wasn't going to happen. If it did happen, we would actually need user interaction to figure out if it matched with a PO in the system. 

If you want an easy way to test this, you can create a dialog that allows you to enter your PurchId and your Format and ensure that the logic above will modify the PurchId the way you intend.
I added this to the job to give me a dialog:

    Dialog                  dialog = new Dialog("Purchase order number");
    DialogField             dlgpurchid,dlgFormat;

    dlgpurchid = dialog.addField(extendedTypeStr(str60),"Num to validate");
    dlgFormat  = dialog.addField(extendedTypeStr(NumberSequenceFormat));
    dlgFormat.value("PO#######"); // Set my current known number sequence as a starting point    

    if(dialog.run())
    {
        purchid = dlgPurchId.value();
        // code to select the num seq record from the table can go here
        //After selecting the numsequencetable record, you would need to store the format from your dialog
        numSeq.format = dlgFormat.value();
        // code to fix the purchid can go here
    }

This next section specifically references where to put the code in AxTension to adjust the PurchId. It has to be done on the header and the lines (as you can pass invoice number per line with AxTension).

1. Add method to AXTip_ImportPurchInvoices that will receive the purchId, fix it, and return the new purchId

2. Add code in method addInvoiceLine() where it calls the setField() for #Field_Line_PurchId. Replace it with this:
this.setField(#Field_Line_PurchId, this.YOURMETHOD(getFieldorDefaultValue(this.getField(#Field_Line_PurchId, ''), this.getField(#Field_PurchId, ''))));

3. Add code in method handlePurchInvoice(). After the if(importRunId), add this line of code:
this.setfield(#Field_PurchId,this.YOURMETHOD(this.getField(#Field_PurchId)));

4. Add code in class AXTip_ImportInvoices_ReadSoftOnline. This extends a class that extends the earlier class where you put your new method to fix the purchId. Add code in the analyzeFile() method.
Set default Id with your new logic.
defaultPurchId = this.YOURMETHOD(this.getfield(#Field_PurchId, ''));

5. In the readsoftonline class in #4, add code in the setHeaderFields() method:
change the this.setField() for the #Field_PurchId to call your new method instead
this.setfield(#Field_PurchId, this.YOURMETHOD(this.marshalStr(documentData.get_PurchId(), identifierStr(PurchId))));

Hope this helps!

-Amber





Friday, October 30, 2020

Designing SSRS reports easier

My first big tip is to always create an AutoDesign report with your data. Use the style template that gives you the pieces you need and then you can always right-click on your AutoDesign and "Create Precision Design" from it. Then you get a precision design where you can just move around the objects. It gives you something to start from so you aren't just drawing text boxes on your screen.

I'm currently working on a project where they always want the Font to be Arial and the size to be 10pt. The default ReportLayoutStyleTemplate uses Segoe UI and the data in the rows are 8pt size. You can find the ReportLayoutStyleTemplate in the AOT. Duplicate it into your project and modify the font and size for the sections you need. When you build your project, you will now see that new template as an option in your AutoDesign Layout Template drop-down. Now when you generate the Precision Design, it will use the font that you set up from that template.

Hope I explained that well.

Good luck!

Thursday, October 22, 2020

AX SSRS Report - How to modify parameter properties based on usage data values

I have a report type parameter that determines which parameters the report needs to run.
I created a new enum for the Report type, available options are Job Number or Invoice Number.

If the job number is selected there will be a start job number and end job number with labels "Starting number" and "Ending number." If Invoice Number is selected there will be a start invoice number and an end invoice number with the exact same labels.

Since the job number is the first enum, I decided to build the parameters as if Job number were selected. However, if the user selects invoice number and that is stored in usage data, the parameter screen would need to accommodate that and show the correct parameters based on the invoice number report type selection.

I created a UI Builder class to accomplish this. My report is already using a contract class to define the 5 parameters I need for the report, a controller class, and a DP class to process the report. In the build() method of the UIBuilder class, I add all the dialog fields I will need and then I set visible = false for the invoice start and end fields.


In the postRun() method (before the super()), I check the value of the reportType enum. PostRun() gets called after the usage data is stored in the field(s). If the reportType is InvoiceNumber, I set the jobstart/end fields to visible = false and the invoicestart/end fields to visible = true.


This works nicely. 

I also had to override the modified method of the reportType parameter in order to change the parameters shown when the user modifies the report type. For this, I used the postBuild() method tell the system to override the modified() method and where to go to find the new logic for this method.


Ensure that you use the right form control for your field type. For example, my enum reportType is a comboboxcontrol. If your field is a string, you would need a FormStringControl. If your field is an Int you would need a FormIntControl, etc. Your modified() method will need to pass that same type of control as well.


Good luck!




Tuesday, July 21, 2020

D365 Visual Studio Application Explorer filters

When I learned that the Application Explorer search window uses regular expressions, it made finding objects so much easier.
Everyone should know that you can use: type:"form" to only find form type elements but if you add regular expressions to it, you can greatly limit the results returned.
For example, you can add: List$ to find only forms that end in the word List or you can add: ^Cust to only find forms that start with Cust.
My favorite is to find objects that start with one word (often the prefix we are using for customizations) and ends with another word. For example, to find all objects that start with MCA and end with the word TEST you can do: ^MCA.*Test$
I use this every single day!
Happy coding!

Friday, October 18, 2019

D365: warehouse locations data migration

When using the standard warehouse locations data entity, I kept getting an error that the "Field WarehouseAisleId must be filled in."

This is odd because the wmslocation table, the entity and the staging table all have the Mandatory property set to "No" for this field.

While investigating, I found that in the WhsLocationBuild class, "createNewLocation()" method, the system sets wmsLocation.aisleid to '--' (yes, it's hard-coded, sigh).

Also, the data entity specifically checks that the field is filled in during the validateWrite() method. To get around this, I added:
'--' as WarehouseAisleId
to my script that exports the data out of my legacy application.

Hope this helps!

Tuesday, October 8, 2019

D365: getting a drop-down/lookup for a new field on a table

So you have a new table that you want to associate with InventLocation. You create the table and add a field to the table. Let's call it TestTable and the field is GroupId. Now you want to add groupId to the inventLocation table and add it to the inventLocation (warehouse) form and get the drop down to show all the groupIds in the TestTable to choose from.

In TestTable, create an index with your GroupId field in it. It should not allow duplicates and it should be used as the primary and clustered index of your table.

Now go to the InventLocation table and create a new relation, choose foreign key relation as the type of relation you are adding. The related table should be TestTable and then choose your index that you created as well.

Once you save this, it will add a field to InventLocation for you and will also create an Index for you. If you want to allow duplicates in InventLocation, you will have to go to the index it created and set allowDuplicates to Yes. You can delete the index if you don't want it though.

That's it!

Wednesday, September 18, 2019

D365: Menu item cannot be opened

I was getting an error on my dimension group forms (Product Information Management > Dimension and variant groups > [Dimension name] group) that said the menu items could not be opened.
In the background there was also this little message saying that the system language was not set. This was happening in a Dev environment using the DAT company/legal entity.
I did a full DB sync and still got the errors. I ended up going to fill in the system language because the pop up was annoying me and that fixed the issue with the menu items.
The setting is under System Administration > Setup > System parameters.

Hope this helps!

Wednesday, September 4, 2019

Dynamics 365: Printing two reports (a docentric and ssrs report) at the same time

I was tasked with creating a "master bill of lading" report that was very similar to the bill of lading report (wmsbilloflading) with only design changes. I decided to use Docentric instead of using SSRS (which the standard wmsbilloflading uses).

There was also a requirement that when the master bill of lading was printed that all the children bill of ladings should be printed as well.

I created my own controller class to handle the reports (it extends WmsBillofLadingController). Both the bill of lading and master used the same controller class. The "print" button on the bill of lading form triggered this controller class (main method).

I created a new PrintMgmtDocumentType for my new report. Because of that, I had to add code to the initPrintMgmtReportRun() method to construct the correct printMgmtReportRun class for my new document type.

I also had to write a handler class for other print management methods that needed handled:





In the newFromArgs() method of my controller, I determined if I was printing the master bol or a standard bol based on the args().record() passed in. If the masterbol flag is on, then I needed to setup the master report; otherwise, setup the standard report:





The startOperation() method will call back into the main() method for subsequent calls to the bill of lading report. It uses the parmArgs() to pass in the new wmsbilloflading record() to print and it uses parm to tell it if we are printing a child or the master record as if it were a child and the parmObject contains the printSettings. We have to pass the printSettings() to the children since we suppress the dialog.

The startOperation() method is where the report Dialog and then report are actually called from the super() method. This is where you can catch it to display multiple reports at once.


There is an issue specific to the master being a docentric report and the children being SSRS reports. After the SSRS reports run and you go to run the Docentric master report again, the print medium now shows "screen" instead of "Docentric screen." So that the user didn't have to keep changing that, I had to add code in the loadPrintSettings() method of the controller so that if it comes in as a master and the screen type is screen, change it to screen_dc. Also, if it is not a master and it comes in as Docentric, I change it to a regular print medium type UNLESS it is screen. If it comes in as Screen, I always want it to print as Docentric screen medium type because it prints out much better to a printer than a SSRS does. 


Let me know if you have any questions.

Happy coding!





Thursday, August 1, 2019

Dynamics 365 - Breakpoint will not be hit. Symbols have not been loaded. . .

I looked all around for a resolution to this problem and everyone pointed to the same setting.
Under Dynamics 365 > Options :
Make sure under the Dynamics 365 > Debugging section, make sure that "Load symbols only for items in the solution" is NOT checked.

So I did that, but still continued to get this error when attempting to debug.

There is another setting that you need to check.

It is in VS under Tools > Options. Go to the Debugging > Symbols section and make sure that Automatically load symbols for :
"All modules, unless excluded" is selected!

Friday, May 4, 2018

AX 2012: Adding a field to PurchLine and the PO confirmation report

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!


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;
}

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();



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!

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;
                    }
....
}

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);
        }
    }

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).

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!

Limiting user sessions

Our company has a limited number of AX user licenses, as most companies will. Some people like to open multiple sessions (and eat up licenses) while they're working. We have asked people to stop this and we close their sessions when we notice that they have multiples open, but we wanted some way for the system to prevent this from happening.

I read about this idea on another blog, but I didn't save the link. It's the same general idea as they proposed, but I did it a little differently, in the end.

I created a table called UserSessionLimits. It has a field called UserID (extended data type is userId) and this is a mandatory field. It has another field called MaxSessions (this is an integer) and it also is mandatory. The index is UserIDx, it contains userid and it is unique.

The code that accesses this table is written so that if a user is not in this table, then they are limited to one session, so add users to this table if they are exceptions and you want them to be able to have more than one session open.

Add code to the "Info" class, in the startup() method. The Info class can only be accessed by going to the System Documentation area of the AOT.

In the startup method, at the top, I placed this code:

select count(recid) from sysClientSessions
where sysClientSessions.userId == curuserid() &&
sysClientSessions.Status == 1 &&
sysClientSessions.clientType == 0;

if(sysClientSessions.RecId > UserSessionLimits::getMaxSessions(curUserid()))
{
box::stop(strfmt("You have exceeded your session limit of %1, the application will now close",
UserSessionLimits::GetMaxSessions(curUserid())));
appl.globalcache().set(classstr(info),identifierStr(AutoLogoff), true);
this.shutDown(true);
}

By searching only for clientType = 0, you will only be looking for "User" sessions. Web sessions or worker sessions (batches that are running) will not be affected.

There have been times that people have gotten around this. They quickly opened two sessions immediately one after the other. If they are simultaneously opened, it's hard to catch. Also, sometimes this locks people out. If they were doing something and AX shut down on them or their system froze, sometimes it takes some time for the session to end for them to get back in again. Your network administrator can control when inactive sessions time out.

We also set automatic shutdown (in user options) to 60 minutes. So if their session is inactive for 60 minutes, it will close.

Update:
So we implemented this and found that sometimes people would see this box and never click OK. It is the user interaction that causes the session to close. So if they just leave the session up with the box open, then they can still open up a bunch of sessions and eat up our licenses. So we changed the code to show a progress box for 5 seconds instead of a box that requires user interaction. I made this a utility in case we ever needed a progress box that would show a message for a period time and then close automatically, we could call this code again. Here is the code:

static void timedProgressBox(int timetoWait, str caption, str text)
{
    #AviFiles
    AppTimer timer = new AppTimer();
    SysOperationProgress  progress = new SysOperationProgress();
    int i ;
    ;

    timer.Start();

    progress.setAnimation(#AviStopWatch);
    progress.setText(text);
    progress.setCaption(caption);
    progress.setTotal(timetoWait);

    for (i = 0; timer.time() < timetoWait; i++)
    {
        timer.ticks();
        progress.setCount(i,1);
        i = real2int(timer.time());
    }

    progress.kill();

    timer.Stop();
}