Pages

Tuesday, August 12, 2014

How to get/set printer settings in AX 2012 for SRS reports

This is a simple job that shows you how to get/set printer settings in AX 2012.  In AX 2009, I used things like PrintJobSettings and SysPrintForm where in AX 2012 you use SRSPrintDestinationSettings which uses SysOperationsTemplateForm.

This is just a "Hello World" if you will for modifying the print settings in AX 2012 and I'll be posting a follow up with a slightly more advanced example.


static void GetPrinterSettingsAX2012Simple(Args _args)
{
    SRSPrintDestinationSettings             printSettings = new SRSPrintDestinationSettings();
    SRSPrintDestinationSettingsContainer    printerNameDestination;
    
    // This sets what the user will see defaulted
    printSettings.printMediumType(SRSPrintMediumType::Printer);
    
    if (SrsReportRunUtil::showSettingsDialog(printSettings))
    {
        printerNameDestination = SrsReportRunUtil::getPrinterNameDestination(printSettings);
        
        if (printerNameDestination != conNull())
        {
            info(strFmt("Printer Name: %1", conPeek(printerNameDestination, 1))); 
            info(strFmt("Printer Destination: %1", conPeek(printerNameDestination, 2)));
        }
    }
    else
        info("User clicked cancel");
}

Friday, August 8, 2014

AX 2012 TFS: Solving update conflicted FK_ModelElementData_HasModelId_LayerId and XU_Update error with SQL profiler

When using AX 2012 with TFS, sometimes you'll get a cryptic error during a TFS synchronization like this:

SQL error description: [Microsoft][SQL Server Native Client 10.0][SQL Server]The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_ModelElementData_HasModelId_LayerId". The conflict occurred in database "Dev5_AX2012_model", table "dbo.Model".
SQL statement: { CALL [Dev5_AX2012_model].[dbo].[XU_Update](?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) }
Cannot execute a stored procedure.




And that's hardly enough information to solve it.

Most likely cause of error for those who don't want to read the entire article:
You probably have a USR layer (or some other layer) that was mistakenly created by any number of things that you should delete.


How to identify/solve:
First, I have to give credit to this post by Fred Shen that got me started down the right path.  He received the same FK_ModelElementData_HasModelId_LayerId error, but with a different root cause.  I couldn't use his steps verbatim though because my TFS sync would take 30+ minutes and that would generate tons of SQL profiler data that I couldn't easily sift though.

  1. Find the error code (547 for me).  SystemAdministration>Inquiries>Database>SQL statement trace log

  2. Launch SQL Profiler and connect to the SQL server
  3. Create new trace, name it whatever you'd like, set a stop time just in case you leave it running, then click on "Events Selection" tab
  4. Uncheck everything
  5. Check "Show all events" and "Show all columns"
  6. Under "Errors and Warnings" check "Exception"
  7. Under "Stored Procedures" check "SP:Starting"
  8. Verify for both of these rows that the columns "TextData", "DatabaseName", and "Error" (where applicable) are checked
  9. Click column filters and on DatabaseName, put your database name or %DBName% for wildcards
  10. Click on the Error filter and put the error code from step 1 (547 in my case)
  11. Lastly, click on TextData and put in "%XU_Update%" and "%FOREIGN KEY%"
  12. Run it, then execute a TFS sync or whatever it is you do to cause the exception to be thrown in AX.
  13. Stop it after you receive the exception and you should see a bright red "Exception" in the profiler.
  14. Press Ctrl+F, and type "exception" and choose the column "EventClass" to search and it'll help you get right to the bad call
Hopefully this helps somebody identify exactly what is throwing their error!


Friday, July 18, 2014

HowTo: Automatically create parm methods on AxBC classes in one line of code!

When you get this best practice error:

"The Table.Field does not have a corresponding parm-method on the AxBC-class"

You can save yourself time and automatically add the methods with this line in a job:

AxGenerateAxBCClass::newTableId(tableNum(WMSPickingRoute)).run();

Now whenever you run code that creates code, you should know what you're doing.  Also, this won't create the set methods, but I think that's a bug on Microsoft's part because part of the code appears to be there, but not used at all.  Look at `\Classes\AxGenerateAxBCClass\createInterfaceMethods` and see the variable `setMethodName` is not used.

I modified that class to make it work for my purposes.

Wednesday, July 16, 2014

Force the batch server to execute batch jobs immediately and bypass the 60 second wait in one line of code! - [AX 2012]

In AX 2012, if you want to force the batch server to immediately check for awaiting batch jobs and execute them without waiting the 60 seconds for the server to discover them, you can call:

xApplication::checkForNewBatchJobs();

Why might you need this?

In my case, I need to call this because I think there is a bug caused by a race condition that can occur with reliable asynchronous (SysOperationExecutionMode::ReliableAsynchronous) processes that are called from the client.

What happens when you call a reliable asynchronous process client side, two things happen at the same time from `\Classes\SysOperationController\doBatch`:

  • Batch task record is inserted to \Data Dictionary\Tables\Batch)
  • Some sort of new thread is spun up asynchronously via `\Classes\SysOperationController\asyncWaitForBatchCompletion`

So, my theory on what can happen is the async polling process (`\Classes\SysOperationFrameworkService\waitForBatchJob`) will try and select the Batch record and may not find it because the Batch record hasn't finished inserting.

In my case, I've overloaded the doBatch() method and put some tracking logic with pessimisticLock that slows down the Batch insert just enough to periodically cause this...so I end up with batch jobs sometimes that are waiting ~60 seconds to be picked up.

Wednesday, July 2, 2014

How to: Copy a sales order using SalesCopying class

There are a million code snippets that show you how to create/copy a sales order that work 80% of the time, but often don't account for the many different scenarios from different environments.

This job shows you how to copy a sales order, but more importantly it shows you how to use the SalesCopying class so that you can copy from SalesQuotes, Journals, etc.


static void JobCopySO(Args _args)
{
    SalesTable  salesTable = SalesTable::find('SO000777');
    SalesLine   salesLine;
    SalesTable  salesTableNew;
    SalesOrderCopyingContract contract = SalesOrderCopyingContract::newIsCreditNote(false);
    
    SalesCopying            salesCopying;
    TmpFrmVirtual           tmpFrmVirtualLines;
    TmpFrmVirtual           tmpFrmVirtualHeader;
    
    void writeTmpFrmVirtual(TmpFrmVirtual _tmpFrmVirtual, TableId _tableId, RecId _recId, Num _id, LineNum _lineNum = 0, TransDate _transDate = systemDateGet(), Qty _qty = 0)
    {
        _tmpFrmVirtual.clear();
        _tmpFrmVirtual.TableNum     = _tableId;
        _tmpFrmVirtual.RecordNo     = _recId;
        _tmpFrmVirtual.Id           = _id;
        _tmpFrmVirtual.LineNum      = _lineNum;
        _tmpFrmVirtual.TransDate    = _transDate;
        _tmpFrmVirtual.Qty          = _qty;

        _tmpFrmVirtual.write();
    }
    
    // Create your new sales header
    salesTableNew.SalesId = NumberSeq::newGetNum(SalesParameters::numRefSalesId()).num();
    salesTableNew.initValue();
    salesTableNew.CustAccount = salesTable.CustAccount;
    salesTableNew.initFromCustTable();
    salesTableNew.insert();
    
    // Build header virtual
    writeTmpFrmVirtual(tmpFrmVirtualHeader, salesTable.TableId, salesTable.RecId, salesTable.SalesId);
    
    while select salesLine
        where salesLine.SalesId == salesTable.SalesId
    {
        writeTmpFrmVirtual(tmpFrmVirtualLines, salesLine.TableId, salesLine.RecId, salesLine.SalesId, salesLine.LineNum, systemDateGet(), salesLine.SalesQty);
    }
    
    contract.parmSalesPurchCopy(SalesPurchCopy::CopyAllHeader);
    contract.parmCallingTableSalesId(salesTableNew.SalesId);
    contract.parmTmpFrmVirtualLines(tmpFrmVirtualLines);
    contract.parmTmpFrmVirtualHeader(tmpFrmVirtualHeader);
    contract.parmQtyFactor(1);
    contract.parmRecalculateAmount(NoYes::No);
    contract.parmReverseSign(NoYes::No);
    contract.parmCopyMarkup(NoYes::No);
    contract.parmCopyPrecisely(NoYes::No);
    contract.parmDeleteLines(NoYes::Yes);    
    
    SalesCopying::copyServer(contract.pack(), false);
       
    info(strFmt("Created %1", salesTableNew.SalesId));
}

Wednesday, June 18, 2014

[AX 2012 Upgrade] Identifying bad EDT relations using reflection and creating custom project with them.

In AX 2012, relations are no longer supported under Extended Data Types.

During an upgrade where you are re-implementing, it's common to import database schema via XPO to get your table structure with EDTs.

Sometimes EDTs that are brought over end up pointing to non-existent tables and/or and you change/rename tables, things get broken.

To uplift your old EDT relations to their corresponding tables, you can use the EDT Relation Migration Tool (http://msdn.microsoft.com/en-us/library/gg989788.aspx) located under Tools>Code Upgrade>EDT Relation Migration Tool.

My first time running this tool, it errored with "The table  does not exist."

Which was due to an EDT that was imported that had a broken table relation.

This job will loop over every CUS+ EDT, and if it has a relation, check if it's valid.  If it is not, it will create a private project of the EDTs with errors.  You can easily change it to search different layers/models/etc.  I only limited to the CUS layer for performance.

static void JobCreateProjWithBrokenEDTs(Args _args)
{
    #define.ProjName('BadEDTs')

    SysProjectFilterRunBase     projectFilter = new SysProjectFilterRunBase();
    ProjectNode                 projectNode;
    UtilElements                utilElements;

    SysModelElement             modelElement;
    SysModelElementType         modelElementType;
    SysModelElementData         modelElementData;

    SysDictType                 sysDictType;
    SysDictRelation             sysDictRelation;
    TableId                     tableId;
    SysDictTable                sysDictTable;
    SysDictField                sysDictField;
    FieldName                   fieldName;
    int                         i;

    void addBadEDT(SysModelElement _modelElement)
    {
        utilElements = null;
        utilElements.Name = _modelElement.Name;
        utilElements.ParentID = _modelElement.ParentId;
        utilElements.RecordType = _modelElement.ElementType;

        if (utilElements.RecordType == UtilElementType::SharedProject ||
            utilElements.RecordType == UtilElementType::PrivateProject ||
            utilElements.RecordType == UtilElementType::ClassInternalHeader ||
            utilElements.RecordType == UtilElementType::TableInternalHeader ||
            !projectFilter.doUtilElements(utilElements))
        {
            info(strfmt("@SYS316339", strfmt('%1 %2', utilElements.RecordType, utilElements.Name)));
        }
    }

    projectFilter.grouping(SysProjectGrouping::AOT);

    while select modelElement
        join Name from modelElementType
            where modelElementType.RecId == modelElement.ElementType    &&
                  modelElementType.RecId == UtilElementType::ExtendedType
        join modelElementData
            where modelElementData.ModelElement == modelElement.RecId   &&
                  modelElementData.Layer        >= (UtilEntryLevel::cus-1)
    {
        sysDictType = new sysDictType(modelElement.AxId);

        if (sysDictType)
        {
            sysDictRelation = sysDictType.relationObject();

            if (sysDictRelation)
            {
                tableId = sysDictRelation.table();

                sysDictTable = new SysDictTable(tableId);

                if (sysDictTable)
                {
                    // Found an EDT with a valid table, check if the field
                    // relations are good
                    for (i = 1; i <= sysDictRelation.lines(); i++)
                    {
                        fieldName = fieldid2name(tableId,sysDictRelation.lineExternTableValue(i));

                        sysDictField = new SysDictField(sysDictTable.id(),fieldname2id(sysDictTable.id(),fieldName));

                        if (!sysDictField)
                        {
                            // Field relation is bad on EDT
                            warning (strFmt("%1 found table, missing field on table %2", sysDictType.name(), sysDictTable.name()));
                            addBadEDT(modelElement);
                        }
                    }

                }
                else
                {
                    // Found an EDT with a broken table relation
                    warning(strFmt("%1 missing valid table", sysDictType.name()));
                    addBadEDT(modelElement);
                }
            }
        }
    }

    SysUpgradeProject::delete(#ProjName, ProjectSharedPrivate::ProjPrivate);
    projectNode = SysTreeNode::createProject(#ProjName);
    projectFilter.parmProjectNode(projectNode);
    projectFilter.write();

    info(strFmt("Created private project %1", #ProjName));
}

Wednesday, June 4, 2014

AX 2012 TFS Synchronizing multiple models at the same time in 1 line of code!

AX TFS version control sync'ing is designed out of the box to allow you to synchronize multiple models, but for some reason Microsoft intentionally disabled this via code.  This is my only concern...why did they intentionally disable it?

I've done some light testing with multiple models and it appears to be working fine synchronizing multiple models.  I've added this code in our development environment with the caveat among the other developers that Microsoft encourages syncing 1 model at a time in code, but syncing multiple seems to work just fine.

Change this one line in \Classes\SysVersionControlUserInterfaceMorphX\promptForFolder to:






And now you can check multiple models to sync at the same time