Friday, 17 July 2015

Dynamics AX 2012 Workflow receives "Failed to find workflow" error

We have run across a few situations in Dynamics AX 2012 where the workflow batch job fails with the error "Failed to find workflow" message.  When the customer received this error it prevented the remaining workflow messages in the queue from being processed.  In order to process all of the items that were queued up we needed to complete the following steps:

1.  Identify the record(s) that are causing the batch to fail.
Select a.RECID from SYSWORKFLOWMESSAGETABLE a Where a.ROOTCORRELATIONID notin(Select b.ROOTCORRELATIONID from SYSWORKFLOWTABLE b)and a.MESSAGELIFECYCLESTATE = 1
2.  Dequeue the SYSWORKFLOWMESSAGETABLE records that do not have a corresponding SYSWORKFLOWMESSAGETABLE record.
Update SYSWORKFLOWMESSAGETABLE Set MESSAGELIFECYCLESTATE = 2 where RECID =<RECID returned from statement above>
The procedures above will dequeue the message causing the error and allow the batch to complete.

Note:  As always, ensure you do a proper backup of your existing SYSWORKFLOWMESSAGETABLE prior to attempting this fix.

Forms not opening in local client.

In support we have seen a number of cases described along the lines of “Forms opens on remote desktop client but not on local AX client.”
Or “Dialog boxes fail to open.“ Historically we have found that the forms that fail to open are not out of the box forms. Instead they are USR or ISV layer objects.

What we have found is that the “Slide open combo boxes” under Performance options needs to be checked.


On server operating systems this is not turned on by default.

You can find this setting by right clicking on Computer -> Properties -> Advanced system settings -> Performance -> Settings.

You receive the error: “The transactions on voucher xxxxxxx do not balance as per xx/xx/xxxx. (Company currency: -x.xx - secondary currency: x.xx)” when attempting to post an invoice proposal in Project management and accounting.

One possible cause of this issue is the duplication of number sequences.  In Project management and accounting, there are separate number sequence setups available for On-Account Invoice, Invoice, On-Account Credit Note and Credit Note document types.  If the number sequences are not uniquely defined, in the posting process, when the records are fetched from the projProposalJour\ProjInvoiceJour it is possible that posting a credit would fetch the corresponding invoice with the same number sequence and vice versa.  This would then result in a situation where the merger of the documents results in an out of balance transaction, and then you would receive the error: "The transactions on voucher xxxxxxx do not balance as per xx/xx/xxxx. (Company currency: -x.xx - secondary currency: x.xx)".
To resolve this issue, you have a few options:
  1. Uniquely define the number sequences with a character segment specific to each document type.
  2. Use the same number sequence for the document types.
  3. You could also resolve the error by bumping up the next number to something that would not be a duplicate.  However, this would be a temporary solution as you would likely run into the error in the future on other documents.

How to enable the setup for user specific font Settings in MS Dynamics AX 2012

Found the following way on how to customize user depended fonts for reports and forms in AX 2012 client:
(1) Open a new developer workspace
(2) Navigate in the AOT to forms, SysUserSetup, designs, design(sys), [tab:tab](sys)], right click [TabPage.Fonts] (sys) and select properties
(3) Set the property visible by default No to Yes
(4) Save and compiling you the form change.
(5) In the AX 2012 client you can now see menu file (Note: blue button top left), tools, options the fonts and set the setting there user specifically

How to force complete CIL recreation in MS Dynamics AX 2012

“Microsoft provides programming examples for illustration only, without warranty either expressed or implied, including, but not limited to, the implied warranties of merchantability or fitness for a particular purpose. This mail message assumes that you are familiar with the programming language that is being demonstrated and the tools that are used to create and debug procedures.”
Prerequisite: Make sure that a full X++ compile was run without errors before

1) Stop all relevant AOS server(s)

2) On your relevant AOS server(s) navigate to the following folder (default) using Windows Explorer:
C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\bin\XppIL

3) Make sure you create a safe copy of the XPPIL folder content to another new local folder on the AOS computer (example: C:\XPPIL_SAVE)

4) Now delete all folders and files inside the folder “C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\bin\XppIL” but keep the folder “XPPIL” itself.

5) Start all AOS server(s)

6) Run a full CIL creation from AOT. This will create a fresh rebuild of all files/ folders inside the XPPIL folder

Happy DAXing
.........

Importing a General Journal using Data Import/Export Framework AX 2012

Here is a tutorial on how to configure Dynamics AX 2012 to import a general journal from a CSV file. The steps below are using Contoso demo data.
  1. Create a new folder on the root of the C: Drive and name it “DIEF”:
  2. Navigate to Data import export framework | Setup | Data import export framework parameters. Click “Browse” next to “Shared working directory” and select the “DIEF” folder we created. Once selected, click “Validate”.
  3. Close the “Data import export framework parameters” form.
  4. Go to Data import export framework | Setup | Source data formats. Enter “GLJOURNAL” for the “Source name” and “Type” = “File. In the parameters on the right side, enter “File format” = “Delimited”, “First row header” = TRUE, “Row delimiter” = “{CR}{LF}”, “Column delimiter” = “Comma {,}”, “Text Qualifier” = “*”, and “Role separator” = “;”. Click “Application”, and then select “CostCenter”, “Department”, and “ExpensePurpose”. Enter “CostCenter-Department-ExpensePurpose” for the “Dimension format” value.

  5. Go to Data import export framework | Setup | Target entities. Click “New” and enter “Entity type” = “Entity”, “Entity” = “Custom”, “Entity name” = “GLJOURNAL”, “Staging table” = “DMFLedgerJournalEntity”, “Entity class” = “DMFLedgerBalanceEntityClass”, and “Target entity” = “DMFLedgerJournalTransEntity”. Close the “Target entites” form.
  6. Go to Data import export framework | Common | Processing group. Type “GLJOURNAL” for the “Group name”, Ctrl+S to save, and click “Entities”.
  7. On the “Select entities for processing group” form, enter “GLJOURNAL” for both the “Entity name” and the “Source data format”. Click “Generate source file”.
  8. On the “Wizard” form, click “Next”. For the “Display data” fields, select the following and put them in the following sequence: JournalName, JournalNum, LineNum, CurrencyCode, TransDate, Voucher, AccountType, LedgerDimension, AmountCurDebit, AmountCurCredit, OffsetAccountType, OffsetLedgerDimension. Click “Generate sample file”.
  9. A .txt file should open, and save it to the root of the C: drive.
  10. Click “Finish” on the “Wizard” form. Close the “Select entities for processing group” form. Close the “Processing group” form.
  11. Go to General ledger | Setup | General ledger parameters. Click “Number sequences”. Right-click “Gene_10” next to “Journal batch number” and click “View details”.
  12. Click “Edit”, change the “_010” to “JN”, and click “Move up”. Click the “General” fast tab and note the “Next” value, in my case “JN000421”.
  13. Close the “Number sequences” form and the “General ledger parameters” form.
  14. Navigate to General ledger | Setup | Journals | Journal names. Select “GenJrn” and right-click the “Acco_18” next to “Voucher series”, and then click “View details”.
  15. On the “Number sequences” form, click “Edit”. In the “Segments” fast tab, click “Add”, select “Constant” for the “Segment”, and type “VN” for “Value”. Move this new segment to the top by clicking “Move up”. Note the next number in the series. In my case, “VN00000038”.
  16. Close all forms.
  17. Open Excel. Click File | Open. Navigate to the C: drive and select the “GLJOURNAL.txt” file (You may need to change the drop menu to “All Files (*.*)”)
  18. On the “Text Import Wizard” form, click “Delimited” and click “Next”. Check the box for “Comma” and click “Finish”.
  19. In line 2, enter the following values for each header:
    1. JournalName = GenJrn
    2. JournalNum = JN000421 (Value from step 12)
    3. LineNum = 1
    4. CurrencyCode = USD
    5. TransDate = 8/19/2013
    6. Voucher = VN00000038 (Value from step 16)
    7. AccountType = Ledger
    8. LedgerDimension = 110180-OU_1-OU_3566-Training
    9. AmountCurDebit = 10
    10. OffsetAccountType = Ledger
    11. OffsetLedgerDimension = 110101-OU_1-OU_3566-Training
  20. Click File | Save As. Click “CSV (Comma delimited)” for the “Save as type” drop-menu.
  21. Close Excel.
  22. Go to Data import export framework | Common | Processing group. Select the line for “GLJOURNAL” and click “Entities”.
  23. Click the folder icon next to “Sample file path”, and select the .csv file from step 20. Click “Generate source mapping”. Close the infolog.
  24. Close the “Select entities for processing group” form.
  25. On the “Processing group” form, select the line for “GLJOURNAL” and click “Get staging data”.
  26. A form for “Create a job ID for the staging data job” should open and populate with a “Job ID”. Click “OK”.
  27. On the “Staging data execution” form, click “Preview”. Verify the columns are correct, and then click “Run”.
  28. Close the infolog.
  29. On the “Processing group” form, click “Copy data to target”. Select the “Job ID” created earlier, and click “OK”. On the “Target data execution” form, click “Run”, then click “OK”.
  30. Close the Infolog.
  31. Go to General ledger | Journals | General journal. Locate the imported journal, and click “Lines”.
  32. Notice the values imported properly, and click Post | Post. The journal posts successfully.
Happy DAXing
............

Dynamics AX Table Caching: Basic Rules

This post provides some basic general guidance to get you started on setting table caching for custom tables, bearing in mind there will be exceptions. This should generally be defined at design time to avoid costly round trips to the database. As explained by Bertrand Caillet from our PFE (Premier Field Engineering) team:
“This is one of the most fundamental feature of the product today. The three tiers architecture of Dynamics AX allows you to define caching on AOS and client. Not using caching properly is the first root cause for performance.”
http://blogs.msdn.com/b/axinthefield/archive/2014/02/18/top-10-issues-discovered-in-the-dynamics-ax-code-review.aspx
Essentially there are two types of table caching as explained on msdn:
Set based caching (AX 2012)
Single record caching (AX 2012)
In AX 2012, table caching is more advanced than in previous versions, including support for joins, unique indexes (as opposed to primary indexes only), cross company queries, etc. (under certain constraints as explained in the above links). So for AX 2009, please see the following links (which generally cover previous versions too):
Set Based Caching (AX 2009)
Single Record Caching (AX 2009)
Cache settings for a table can be found in the following location in the application:
AOT > Data Dictionary > Tables > [TableName] > Properties > CacheLookup
Changes like this should be made by a developer in accordance with best practice guidance.
You can use the script at the bottom of this post to check cache lookup settings for all tables using the “Performance Analyser 1.20 for Microsoft Dynamics” (DynamicsPerf) tool (partly based on the analysis scripts that come with this tool).
Set the appropriate table group depending on how the table is used; see the following article for further details for AX 2012 (for previous versions it is basically the same but with fewer table groups):

Table and table group reference [AX 2012]
http://technet.microsoft.com/en-us/library/gg731855.aspx
Following that, you can generally set table caching according to the table below, again bearing in mind there can be exceptions. Please refer back to the above links for an explanation of each cache lookup type.
Table GroupCache Lookup
Miscellaneous* See notes below
 Parameter EntireTable
 Group Found
 Main Found
 Transaction NotInTTS
 WorksheetHeader NotInTTS
 WorksheetLine NotInTTS
 Framework N/A
 Reference Found
 Worksheet NotInTTS
 TransactionHeader NotInTTS
 TransactionLine NotInTTS
 * All newly created tables default to a table group of Miscellaneous. Ideally don’t use this table group for custom tables.
Finally, bear in mind that in AX 2012, the cache limit is configurable for every table group in the server performance settings:
System Administration > Setup > System > Server Configuration > Performance optimisation tab
Entire table cache size determines in kilobytes how much data is cached in memory before spilling to disk. The defaults are 32KB for AX 2012 RTM and 96KB for AX 2012 R2/R3.
The record cache limits define (per table group) the number of records stored in the server side cache and the client record cache factor defines based on that the number records stored in client cache, e.g. server side cache of 2000 (default) and client record cache factor of 20 (default) means 100 records are stored in client cache. Each AOS server can have its own cache settings. The basic rule here it is to keep the defaults unless performance testing proves it addresses a specific issue.

Happy DAXing
........

Thursday, 16 July 2015

Customizing the system-generated query of the Invoice journal form

When creating a Sales order in Dynamics AX 2012, you can define both a "Customer account" and an "Invoice account". The Customer account identifies the customer for which the Sales order is being created, and the Invoice account references the account number of the customer to invoice, in case it is different.


If you open the Invoice journal form from the Customer list page, you see only the transactions that were created for the customer selected in the grid, and not the transactions that were invoiced to this customer:
1- Go to Accounts receivable > common > Customers > All customers
2- In the ribbon, click the Invoice tab
3- Click the Invoice journal button in the Journals button group




If you want to modify this behavior, you will need to modify the query that is executed when the CustInvoiceJournalform is opened.
This is a good example of customization that would require accessing and modifying the system-generated query of a form, like explained here: https://msdn.microsoft.com/en-us/library/aa659696.aspx.
Setting the AutoQuery property on a form data source to Yes - as in the case here - causes the system to automatically generate the query that retrieves data to display in the form.
In this scenario, modifying the system-generated query to display a different set of data can be done in the init method of the data source of the form (in bold below):
Form: CustInvoiceJournal
Data source: CustInvoiceJour
Method: init
public void init()
{
   QueryBuildDataSource   queryDataSourceLink;
   CustInvoiceJour              custInvoiceJourLoc;
   SalesTable                      salesTableLoc; 
   TAMDeduction           tamDeduction;
   CustTable              custTable;
   super();
   if (element.args().dataset() == tableNum(CustTable) && element.args().record().(fieldNum(CustTable, AccountNum)))
   {   
      custTable = element.args().record();
      this.query().dataSourceTable(tablenum(CustInvoiceJour)).clearDynalinks();    
      this.query().dataSourceTable(tableNum(CustInvoiceJour)).addRange(fieldNum(CustInvoiceJour, RecId)).value(strFmt('((OrderAccount == "%1") || (InvoiceAccount == "%1"))', custTable.AccountNum));
   }
(…)

I hope this is helpful!

Happy DAXing
..........

X++ code to Count Records in Query

Following code illustrates how we can use SysQuery::countTotal() method to get the number of records in Query.

static void Query_cntRecords(Args _args)
{
    Query                query = new Query();
    QueryRun             queryRun;
    QueryBuildDataSource qbd;
    ;
   
    qbd = query.addDataSource(tablenum(CustTable));
    queryRun = new QueryRun(query);
   
    info(strfmt("Total Records in Query %1",SysQuery::countTotal(queryRun)));
   
}


No of ways to count the records

From data source
datasource.numberOfRowsLoaded()
From select queryselect count(RecId) from tableA;

Fetch number of records in a FormDataSource (e.g. of a Grid)

SysQuery::getTotal works fine, but the trick is how to handle temporary data sources where getTotal does not work:
if(!formDataSource.curser().isTmp())
{
    recordCount = SysQuery::getTotal(formDataSource.queryRun());
}
else
{
    while(!formDataSource.allRowsLoaded())
    {
        formDataSource.getNext();
    }
    recordCount = formDataSource.numberOfRowsLoaded();
}
Now recordCount contains the number of Records in the FormDataSource irrespective of the Tmp status of the data source. Of course the whole tmp-data source has been loaded in the process, which might be an issue in some cases.

Happy DAXing
.............

AOT Synchronization Errors (Troubleshooting )

Illegal data conversion from original field VendTable.ciscompanyregnum to vendtable.usecashdisc: unable to convert data types to anything but character field type (0 to 4)

During the Synchronization process in Dynamics AX 2012 there can be times when you experience errors which relate directly to SQL server statement that need to be handled and resolved. This blog has a number of tips you can use to help troubleshoot some of the more common errors.  You can get Synchronization errors in the course of performing multiple tasks; Initialization checklist, full version upgrade, applying rollups, service packs or cumulative updates, moving model files or code, and in applying License key or configuration keys changes.
It is important to understand why the synchronization was triggered and if the right approach is even to continue with the synchronization or if you triggered the synchronization in error, such as missing pre-processing upgrade tasks or doing things in the wrong order.
The right approach to use in each case may be different and may not necessarily be the steps described below, but at least they may help you in investigating and understanding the underlying problem. The errors in the infolog may not contain the whole error message so get the exact details from the application event logs of the AOS server you are connected to.  Normally the synchronize error is SQL statement that cannot be executed at the database and cannot be resolved without some action taken either in the database or in the AOT.
The error I will focus on is the following:
Illegal data conversion from original field VendTable.ciscompanyregnum to vendtable.usecashdisc: unable to convert data types to anything but character field type (0 to 4)
If you look at the issue at its surface the problem is that it can’t convert data types, but you need to understand this in the context of what the user is trying to accomplish.  In this case they wanted to remove the SYP layer code which was from CU3, before upgrading to the Feature Pack and then upgrading to CU3.  In the RTM code both columns exist, but it didn’t make sense that it would need to do a data conversion on these two fields.
To investigate this further we needed to understand why it was trying to do this seemingly incorrect conversion, but to accomplish this you need to know how the AOS determines what needs to be synchronized.  The AOS looks at the AOT to see what is currently in the code and compares it to a Dynamics AX table called SQLDictionary which holds information about the state of objects that also exist in SQL server.
To get the information about the VendTable in the error we created the following SQL statement:
select * from sqldictionary where TABLEID in (select TABLEID from sqldictionary where name = 'VendTable')
This will give you the fieldids which of each object, which represent the objects ids when you look at the fields in the AOT properties.
When we checked the object id in his AOT environment in the developer workspace the id didn’t match the value we found in the SQLDictionary table


To resolve the synchronization error we changed fieldid in the SQLDictionary to match what showed in the AOT.
                     UPDATE SQLDICTIONARY
                     SET FIELDID = '60039'
                     WHERE SQLDICTIONARY.RECID = 5638807121;
In the course of working through the rest of the errors we found a different type of issue.  This error was as follows:
Alter table DBO.CUSTINVOICELINESCRIPTBUFFER
ADD CONSTRAINT I_100412RECID Cannot add CONSTRAINT I_100412RECID because it already exists in the database
This would normally be a simple case of finding the Constraint or Index in SQL management studio on the CUSTINVOICELINESCRIPTBUFFER table and deleting it, so the synchronize could add it back, but when we checked that constraint or index didn’t exist on the CUSTINVOICELINESCRIPTBUFFER table, so we had to use a different approach.
Since we knew we could trust the SQL error message we checked the SQL server system tables to help identify the problem with the following statement.
select * from sysobjects where id in (select parent_obj from sysobjects where name = 'I_100412RECID')
 This will give you the name of the SQL table where that Constraint or Index is already in use; in this case it was on an USR layer object that was removed from the environment, but where the underlying table still existed in SQL.  In this case we just dropped the custom table as it was no longer in the AOT.
If you are getting synchronization errors that do no make sense or that you think you should have already resolved there may be old records left in the SQLsyncinfo table or have old or incorrect object ID references.  If you suspect this make a database backup, shutdown the AOS, in SQL run truncate table SQLSYNCINFO, start the AOS and run a new Synchronize from the AOT.
Hopefully this will give you an understanding of some troubleshooting steps that you can use but you need to be careful about changing things in the SQL server database and the SQLDictionary table in a production system.  Although we were able to resolve the problems using the steps above the correct method may have been to do a full X++ compile, full CIL generation, and database synchronization after removing the model files and before attempting the feature pack upgrade.
Always make sure that you are testing these steps against a copy of the production database in a test environment and that you have a good SQL backup.  You want to sure that even in the test environment you can restore to a backup in case you take the wrong approach and so that you can perfect your steps before making any changes against the production database

Happy DAXing
............

Wednesday, 15 July 2015

Deploy the default reports [AX 2012]

Complete the following procedure to open Windows PowerShell and view a list of the reports that are included with Microsoft Dynamics AX.
  1. Open Windows PowerShell as an administrator by following these steps:
    1. Click Start > Administrative Tools.
    2. Right-click the Microsoft Dynamics AX 2012 Management Shell option.
    3. Click Run as administrator.
  • To deploy a specific report, enter the name of the report. For example, to deploy the CustTransList report, enter the following command:
    Publish-AXReport -ReportName CustTransList
  • To deploy two or more specific reports, enter the names of the reports. For example, to deploy the CustTransList and CustTransOpenPerDate reports, enter the following command:
    Publish-AXReport -ReportName CustTransList, CustTransOpenPerDate
    
  • To deploy all reports, enter the following command:
    Publish-AXReport –ReportName *

Restart the Reporting Services service

If you’re running Reporting Services 2008 (in either native or SharePoint integrated mode) or Reporting Services 2012 or 2014 in native mode, restart the service by following these steps:
  1. Click Start > Administrative Tools > Services to open the Services management console.
  2. Right-click the SQL Server Reporting Services service and choose Restart.

Refresh the AOD cache

Complete the following steps to refresh the application object directory (AOD) cache. This will clear cached application object information for all items in the application object tree (AOT) of Microsoft Dynamics AX.
  1. Open the Microsoft Dynamics AX client.
  2. Open the development workspace.
  3. Click Tools > Caches > Refresh elements.
    After the cache has been cleared, the Infolog displays a message indicating that AOD elements have been refreshed.

Instruct users to delete usage data

Instruct Microsoft Dynamics AX users to delete usage data. Selections that users make while Microsoft Dynamics AX is running are saved as usage data. This data is used to improve the users’ experiences the next time that they run Microsoft Dynamics AX.
Instruct users to delete usage data by following these steps:
  1. Open the Microsoft Dynamics AX client.
  2. Click File > Tools > Options. The Options form is displayed.
  3. Click Usage data.
  4. On the General tab, click Reset.

Select Statement Examples [AX 2012]

General Examples


The following X++ job shows several small examples of how you can use the select statement.
static void SelectRecordExamples3Job(Args _args)
{
    CustTable custTable;  // As of AX 2012.

    // A customer is found and returned in custTable
    select * from custTable;
    info("A: " + custTable.AccountNum);

    // A customer with account number > "100" is found
    select * from custTable
        where custTable.AccountNum > "100";
    info("B: " + custTable.AccountNum);

    // Customer with the lowest account number > "100" found:
    select * 
        from custTable 
            order by accountNum
                where custTable.AccountNum > "100";
    info("C1: " + custTable.AccountNum);

    // The next customer is read
    next custTable;
    info("C2: " + custTable.AccountNum);

    // Customer with higest account number
    // (greater than 100) found: Fourth Coffee
    select * 
        from custTable 
            order by accountNum desc
                where custTable.accountNum > "100";
    info("D1: " + custTable.AccountNum);
    
    // The next record is read (DESC): Fabrikam, Inc.
    next custTable; 
    info("D2: " + custTable.AccountNum);

    // Customer with highest account number found: Fourth Coffee
    select reverse custTable 
        order by accountNum;
    info("E: " + custTable.AccountNum);

    // Customer with "lowest" name and account number
    // in the interval 100 to 1000 is found. This is Coho Winery.
    select * 
        from custTable 
            order by DlvMode
                where custTable.accountNum > "100"
                    && custTable.accountNum < "1000";
    info("F: " + custTable.AccountNum);

    // The count select returns the number of customers.
    select count(AccountNum) 
        from custTable;
    // Prints the result of the count
    info(strFmt("G: %1 = Count of AccountNums", custTable.accountNum));

    // Returns the average credit max for non-blocked customers.
    select avg(CreditMax) 
        from custTable
            where custTable.blocked == CustVendorBlocked::No;
    // Prints the result of the avg
    info(strFmt("H: %1 = Average CreditMax", custTable.CreditMax));
}
/*** Display from infolog:
Message (02:00:34 pm)
A: 4000
B: 4000
C1: 4000
C2: 4001
D1: 4507
D2: 4506
E: 4507
F: 
G: 29 = Count of AccountNums
H: 103.45 = Average CreditMax
***/

Join Sample


This X++ code sample shows how an inner join can be performed as part of an SQL select statement.
The sample also shows an order by clause that has each field qualified by a table name. This enables you to control how the retrieved records are sorted by using only oneorder by clause.
static void SelectJoin22Job(Args _args)
{
    CustTable xrecCustTable;
    CashDisc xrecCashDisc;
    struct sut4;

    sut4 = new struct("str AccountNum; str CashDisc; str Description");

    while select firstOnly10 *
        from xrecCustTable
            order by xrecCashDisc.Description
                join xrecCashDisc
                    where xrecCustTable.CashDisc ==
                        xrecCashDisc.CashDiscCode
                        && xrecCashDisc.Description LIKE "*Days*"
    {
        sut4.value("AccountNum", xrecCustTable.AccountNum );
        sut4.value("CashDisc", xrecCashDisc.CashDiscCode );
        sut4.value("Description", xrecCashDisc.Description );

        info(sut4.toString());
    }
/*********  Actual Infolog output
Message (02:29:37 pm)
(AccountNum:"1101"; CashDisc:"0.5%D10"; Description:"0.5% 10 days")
(AccountNum:"4001"; CashDisc:"0.5%D10"; Description:"0.5% 10 days")
(AccountNum:"1102"; CashDisc:"0.5%D30"; Description:"0.5% 30 days")
(AccountNum:"1201"; CashDisc:"0.5%D30"; Description:"0.5% 30 days")
(AccountNum:"2211"; CashDisc:"0.5%D30"; Description:"0.5% 30 days")
(AccountNum:"1202"; CashDisc:"1%D15"; Description:"1% 15 days")
(AccountNum:"1203"; CashDisc:"1%D07"; Description:"1% 7 days")
(AccountNum:"2212"; CashDisc:"1%D07"; Description:"1% 7 days")
(AccountNum:"2213"; CashDisc:"1%D07"; Description:"1% 7 days")
(AccountNum:"2214"; CashDisc:"1%D07"; Description:"1% 7 days")
*********/
}

Group By and Order By


This X++ code sample shows that the fields in the group by clause can be qualified with a table name. There can be multiple group by clauses instead of just one. The fields can be qualified by table name in only one group by clause. Use of table name qualifiers is recommended.
The order by clause follows the same syntax patterns that group by follows. If provided, both clauses must appear after the join (or from) clause, and both must appear before the where clause that might exist on the same join. It is recommended that all group by and order by and where clauses appear immediately after the last join clause.
static void SelectGroupBy66Job(Args _args)
{
    CustTable xrecCustTable;
    CashDisc xrecCashDisc;
    struct sut4;

    sut4 = new struct("str AccountNum_Count; str CashDisc; str Description");

    while select
        count(AccountNum)
        from xrecCustTable
            order by xrecCashDisc.Description
                join xrecCashDisc
        group by
            xrecCashDisc.CashDiscCode
                    where xrecCustTable.CashDisc ==
                        xrecCashDisc.CashDiscCode
                        && xrecCashDisc.Description LIKE "*Days*"
    {
        sut4.value("AccountNum_Count", xrecCustTable.AccountNum );
        sut4.value("CashDisc", xrecCashDisc.CashDiscCode );
        sut4.value("Description", xrecCashDisc.Description );

        info(sut4.toString());
    }
/*********  Actual Infolog output
Message (02:45:26 pm)
(AccountNum_Count:"2"; CashDisc:"0.5%D10"; Description:"")
(AccountNum_Count:"3"; CashDisc:"0.5%D30"; Description:"")
(AccountNum_Count:"4"; CashDisc:"1%D07"; Description:"")
(AccountNum_Count:"1"; CashDisc:"1%D15"; Description:"")
(AccountNum_Count:"1"; CashDisc:"2%D30"; Description:"")
(AccountNum_Count:"1"; CashDisc:"3%D10"; Description:"")
*********/
}


Happy DAXing
........

delete_from [AX 2012]

You can delete multiple records from a database table by using a delete_from statement. This can be more efficient and faster than deleting one record at a time by using thexRecord .delete method in a loop.
If you have overridden the delete method, the system interprets the delete_from statement into code that calls the delete method one time for each row that is deleted.

Example 1: Comparing Multi-record Delete Techniques


The code examples in this section show that some techniques for deleting multiple records are more efficient than other techniques.

Aa624886.collapse_all(en-us,AX.60).gifExample 1a: Delete_From to Delete Multiple Records

The following X++ code example is an efficient way to delete multiple records.
static void DeleteMultiRow1aJob(Args _args)
{
    MyWidgetTable tabWidget;
    ;
    delete_from tabWidget
        where tabWidget .quantity <= 100;
}

Aa624886.collapse_all(en-us,AX.60).gifExample 1b: Delete Method Plus ForUpdate Keyword

The following X++ code example is inefficient. It issues a separate SQL delete call to the database server for each record. The xRecord .delete method never deletes more than one record per call.
static void DeleteMultiRow1bJob(Args _args)
{
    MyWidgetTable tabWidget; // extends xRecord.
    ;
    ttsBegin;
    while select
        forUpdate
        tabWidget
        where tabWidget .quantity <= 100
    {
        tabWidget .delete();
    }
    ttsCommit;
}

Example 2: Delete Plus Inner Join Logic


X++ does not support an inner join on the delete_from statement. Therefore you cannot use the unmodified join keyword on the delete_from statement. However, there are other ways to logically accomplish an inner join.
The examples in this section show techniques for achieving inner join logic by a sequence of X++ statements.

Aa624886.collapse_all(en-us,AX.60).gifExample 2a: Delete Method Plus Inefficient Inner Join Logic

The following X++ code example is inefficient. It issues a separate SQL delete call to the database server for each record.
static void DeleteInnerJoin2aJob(Args _args)
{
    MyWidgetTable tabWidget; // extends xRecord.
    ;
    ttsBegin;
    while select
        forUpdate
        tabWidget
        join tabGalaxy
            where
                tabWidget .GalaxyRecId == tabGalaxy .RecId
                && tabGalaxy .isTrusted == 0
    {
        tabWidget .delete();
    }
    ttsCommit;
}

Aa624886.collapse_all(en-us,AX.60).gifExample 2b: Delete_From Plus Efficient Inner Join Logic

The following X++ code example is relatively efficient. It issues a separate delete_from statement for each loop iteration. However, each delete_from statement can delete multiple records, a subset of all the records that the job deletes.
static void DeleteInnerJoin2bJob(Args _args)
{
    MyWidgetTable tabWidget; // extends xRecord.
    ;
    ttsBegin;
    while select
        from tabGalaxy
            where tabGalaxy .isTrusted == 0
    {
        delete_from tabWidget
            where tabWidget .GalaxyRecId ==
                  tabGalaxy .RecId;
    }
    ttsCommit;
}

Example 3: Delete_From Notexists Join


You can use the notexists join keyword pair in a delete_from statement.
The delete_from statements in the following X++ code example are efficient. The notexists join clause enables the delete_from statement to delete a specific set of rows. In this example the delete_from statement removes all the parent order header rows for which there are no child order line rows.
You can also use the exists join clause on the delete_from statement.
static void DeleteFromNotexists3bJob(Args _args)
{
    GmTabOrderHeader tabOHeader;
    GmTabOrderLine tabOLine;
    AddressState tabAddressState;
    str 127 sOH_Info;
    str 127 sOL_Data;
    int64 i64OHRecId;
    ;
    delete_from tabOLine;
    delete_from tabOHeader;

    // Inserts into parent table.

    sOH_Info = "Albert needs tires.";
    insert_recordset tabOHeader
        (OH_Info)
        select firstOnly sOH_Info from tabAddressState;

    sOH_Info = "Benson wants plastic.";
    insert_recordset tabOHeader
        (OH_Info)
        select firstOnly sOH_Info from tabAddressState;

    // Obtain a OrderHeader RecId,
    // use it to insert one child row.

    sOL_Data = "4 re-treads.";
    while select firstOnly tabOHeader
            order by OH_Info
            where tabOHeader .OH_Info like "A*"
    {
        i64OHRecId = tabOHeader .RecId;
        insert_recordset tabOLine
            (OL_Data ,OrderHeaderRecId)
            select firstOnly
                sOL_Data ,i64OHRecId
                from tabAddressState;
        break;
    }

    // Before the delete notexists.
    // Display all parent, and then all child rows.

    while select tabOHeader
        order by OH_Info
    {
        info(strFmt(
            "Before: OHeader:  OH_Info==%1 , RecId==%2"
            ,tabOHeader .OH_Info ,tabOHeader .RecId
            ));
    }
    while select tabOLine
        order by OL_Data
    {
        info(strFmt(
            "Before: OLine:  OL_Data==%1 , OrderHeaderRecId==%2"
            ,tabOLine .OL_Data ,tabOLine .OrderHeaderRecId
            ));
    }

    // Delete_From NotExists Join, to remove from the
    // parent table all order headers without children.

    delete_from tabOHeader
        notexists join tabOLine
            where tabOHeader .RecId ==
                tabOLine .OrderHeaderRecId;

    info(strFmt
        ("%1 is the number of childless OHeader records deleted."
        ,tabOHeader.rowCount()));

    // After the delete notexists.
    // Display all parent, and then all child rows.

    info("- - - - - - - - - - - - - - -");
    while select tabOHeader
        order by OH_Info
    {
        info(strFmt(
            "After: OHeader:  OH_Info==%1 , RecId==%2"
            ,tabOHeader .OH_Info ,tabOHeader .RecId
            ));
    }
    while select tabOLine
        order by OL_Data
    {
        info(strFmt(
            "After: OLine:  OL_Data==%1 , OrderHeaderRecId==%2"
            ,tabOLine .OL_Data ,tabOLine .OrderHeaderRecId
            ));
    }
/**************  Actual Infolog output
Message (12:54:14 pm)
Before: OHeader:  OH_Info==Albert needs tires. , RecId==5637144608
Before: OHeader:  OH_Info==Benson wants plastic. , RecId==5637144609
Before: OLine:  OL_Data==4 re-treads. , OrderHeaderRecId==5637144608
1 is the number of childless OHeader records deleted.
- - - - - - - - - - - - - - -
After: OHeader:  OH_Info==Albert needs tires. , RecId==5637144608
After: OLine:  OL_Data==4 re-treads. , OrderHeaderRecId==5637144608
**************/
}

Happy DAXing
........

insert_recordset [AX 2012]

The ListOfFields in the destination table must match the list of fields in the source tables. Data is transferred in the order that it appears in the list of fields. Fields in the destination table that are not present in the list of fields are assigned zero-values as in other areas in X++. System fields, including RecId, are assigned transparently by the kernel in the destination table.
insert_recordset  DestinationTable  (  ListOfFields  )
select  ListOfFields1  from  SourceTable  [ where  WhereClause  ]
[ join  ListOfFields2  from  JoinedSourceTable 
[ where  JoinedWhereClause  ]]

Example 1: Sub-select that has a Group By


The records, myNum and mySum, are retrieved from the table anotherTable and inserted into the table myTable. The records are grouped according to myNum, and only themyNum records with a value less than or equal to 100 are included in the insertion.
insert_recordset myTable (myNum, mySum)
    select myNum, sum(myValue) 
        from anotherTable 
        group by myNum 
        where myNum <= 100;

Example 2: Comparing the Performance of Traditional Inserts and Insert_recordset Inserts


By comparing the following two X++ code examples, you can see the performance implications of different designs.

Aa635694.collapse_all(en-us,AX.60).gifExample 2a: Copy Data for Each Record

The following code example shows the traditional way of copying data.
ttsBegin;
while select * from sqlDictionary
    where sqlDictionary.tabId > 0
{
    bufferDictionary.tabId        = sqlDictionary.tabId;
    bufferDictionary.fieldId      = sqlDictionary.fieldId;
    bufferDictionary.array        = sqlDictionary.array;
    bufferDictionary.name         = sqlDictionary.name;
    bufferDictionary.sqlName      = sqlDictionary.sqlName;
    bufferDictionary.fieldType    = sqlDictionary.fieldType;
    bufferDictionary.strSize      = sqlDictionary.strSize;
    bufferDictionary.shadow       = sqlDictionary.shadow;
    bufferDictionary.rightJustify = sqlDictionary.rightJustify;
    bufferDictionary.nullable     = sqlDictionary.nullable;
    bufferDictionary.flags        = sqlDictionary.flags;
    bufferDictionary.insert();
    countTables++;
    operationProgress.setCount(countTables);
}
ttsCommit;

Aa635694.collapse_all(en-us,AX.60).gifExample 2b: Set Based Operation

The following code example achieves the same outcome as the previous example, but this example is much faster. This example uses the insert_recordset statement.
void copySQLDictionary2DictionaryLine()
{
    SqlDictionary sqlDictionary;
    ;
 
    ttsBegin;
    insert_recordset bufferDictionary(tabId, fieldId, array, 
        name, sqlName, fieldType, strSize, shadow, 
        rightJustify, nullable, flags)
    select tabId, fieldId, array, name, sqlName, fieldType, 
        strSize, shadow, rightJustify, nullable, flags 
        from sqlDictionary
        where sqlDictionary.tabId > 0;
    ttsCommit;
}

Aa635694.collapse_all(en-us,AX.60).gifPerformance Comparison Between 2a and 2b

When copying 14,311 rows, the number of trips to the SQL backend has been reduced from 14,213 to 6. The SQL backend time used has been reduced from 174 seconds to 10 seconds, which is a factor of 17. The overall reduction in time is larger because the new approach uses almost no resources from either the client or the Application Object Server (AOS).

Example 3: Data Assignments from Variables


This X++ code example shows that the insert_recordset statement can insert data that is provided in variables. In this example, the keyword firstonly is used so that only one row is inserted.
static void InsertVariable3Job(Args _args)
{
    TableAlphabet    tabA2;
    BankAccountTable tabB3;
    str  1 sLetter = "a";
    str 16 sExampleWord = "apple";
    ;
    DELETE_FROM tabA2;

    INSERT_RECORDSET tabA2
        (Letter ,ExampleWord)
    select firstonly
        sLetter ,sExampleWord // Variables.
    from tabB3;

    WHILE SELECT * from tabA2
    {
        info(tabA2 .Letter + " , " + tabA2 .ExampleWord);
    }
/***********  Actual Infolog output
Message (04:03:52 pm)
a , apple
***********/
}

Example 4: Joins


The following X++ code example shows a join of three tables on an insert_recordset statement that has a sub-select. Also, a while select statement with a similar join is shown.
A variable is used to supply the inserted value for one column. The str variable must be declared with a length that is less than or equal to the maximum length of the corresponding database field.
static void InsertJoin42Job(Args _args)
{
    GmTabDepartment tabDept2;
    GmTabEmployee tabEmpl3;
    GmTabProject tabProj4;
    GmTabEmployeeProject tabEmplProj5;
    str 64 sDescriptionVariable = "From variable.";
    ;
    DELETE_FROM tabEmplProj5;

    INSERT_RECORDSET tabEmplProj5
        (
        Description
        , EmployeeRecId
        , ProjectRecId
        )
    Select
        sDescriptionVariable
        , RecId
    from
        tabEmpl3
        join
            tabDept2
            where tabEmpl3 .DepartmentGuid == tabDept2 .DepartmentGuid
        join RecId
            from tabProj4
            where tabDept2 .DepartmentGuid == tabProj4 .DepartmentGuid
    ;

    info(int642str(tabEmplProj5 .rowCount())
        + " ==Number of rows inserted.");

    WHILE SELECT *
        from
            tabEmplProj5
            join tabEmpl3
                where tabEmplProj5 .EmployeeRecId == tabEmpl3 .RecId
            join tabProj4
                where tabEmplProj5 .ProjectRecId == tabProj4 .RecId
    {
        info(
            tabEmpl3 .EmployeeName
            + "  --works on--  "
            + tabProj4 .ProjectName
            + " (" + tabEmplProj5 .Description + ")."
            );
    }
/*****************  Actual Infolog output
Message (01:05:41 pm)
4 ==Number of rows inserted.
Alice  --works on--  Project ZZZ (From variable.).
Alice  --works on--  Project YY (From variable.).
Beth  --works on--  Project ZZZ (From variable.).
Beth  --works on--  Project YY (From variable.).
*****************/
}


Happy DAXing
........