Dynamics Ax creating a batch job from code

Hi,

Here is a simple code snippet to create Batch jobs from code. This convenient when starting a heavy load job from a user interface and still keep the client responsive.

TSTSalesOrderUpdate tSTSalesOrderUpdate;
 
BatchInfo           batchInfo;
BatchHeader         batchHeader;
;
 
tSTSalesOrderUpdate = TSTSalesOrderUpdate::construct();
 
batchInfo   = tSTSalesOrderUpdate.batchInfo();
batchInfo.parmCaption("Test from code");
batchInfo.parmGroupId("");
 
batchHeader = BatchHeader::construct();
batchHeader.addTask(tSTSalesOrderUpdate);
batchHeader.save();

Dynamics Ax Creating sales orders with the SalesAutoCreate class

Many projects use an interface to import their sales orders, because of this a SalesAutoCreate class was created. This class is easily extendable and customizable.The first thing to do is designing a buffer table, like this one for example:

Sales order import table
Sales order import table

After this we can start extending a new class from the SalesAutoCreate class and modifying the construct on the SalesAutoCreateClass .

class TSTSalesAutoCreate extends SalesAutoCreate
{
    TSTSalesImport  TSTSalesImport;
}
static  SalesAutoCreate    construct(Common       buffer = null,
                                     Object       object = null,
                                     Common       buffer2 = null)
{
    switch (buffer.TableId)
    {
        case tablenum(SalesCreateReleaseOrderLineTmp)     : return new SalesAutoCreate_ReleaseOrder(buffer,object,buffer2);
        case tablenum(PurchLine)                          : return new SalesAutoCreate_ProjPurchLine(buffer,object);
        case tablenum(SalesBasketLine)                    : return new SalesAutoCreate_Basket(buffer,object);
        // TST
        case tablenum(TSTSalesImport)                     : return new TSTSalesAutoCreate(buffer,object);
        // TST
    }
    throw error(strfmt("@SYS23419",tableid2name(buffer.TableId)));
}

After the construct your buffer record should be passed in the new or a parm method could also be an option.

void new(Common _initFromBuffer, Object _callBackClass)
{;
    super(_initFromBuffer,_callBackClass);
 
    TSTSalesImport  = _initFromBuffer;
}

When extending from the SalesAutoCreate class some methods must be implemented:

  • setCust
  • salesType
  • recordExist
  • nextRecord
  • invoiceAccount

In my example I’ve implemented them like this:

protected void setCust()
{;
    custTable   = CustTable::find(TSTSalesImport.CustAccount);
 
    if(!custTable)
    {
        throw error("Custtable not found!");
    }
}
 
protected SalesType salesType()
{;
    return SalesType::Sales;
}
 
protected boolean recordExist()
{;
    return TSTSalesImport.RecId != 0;
}
 
protected void nextRecord()
{;
    next TSTSalesImport;
}
 
protected CustInvoiceAccount invoiceAccount()
{
    CustInvoiceAccount  ret = custTable.InvoiceAccount;
    ;
 
    if(!ret)
    {
        ret = custTable.AccountNum;
    }
 
    return ret;
}

The next step is setting our table and line fields by overriding the setSalesTable and setSalesLine methods and make sure that you always call the super first. Notice that you need to call the createSalesTable and createSalesLine to do the insert.

protected void setSalesTable()
{;
    super();
 
    this.createSalesTable();
}
 
protected void setSalesLine()
{;
    super();
 
    salesLine.ItemId    = TSTSalesImport.ItemId;
    salesLine.itemIdChanged();
    salesLine.SalesQty  = TSTSalesImport.SalesOrderedQty;
 
    this.createSalesLine();
}

you should have a class looking like this.

Sales auto create class
Sales auto create class

the final step is to call the logic from a job or a RunBaseBatch class, make sure that you select records on the same tier as the nextRecord will run or else it will fail. Preferably on the server tier. 🙂

select forupdate TSTSalesImport;
 
SalesAutoCreate = SalesAutoCreate::construct(TSTSalesImport);
SalesAutoCreate.create();

This example lacks some validation whether the record has already been processed or not, so it will created the same records every time it is called. You could implement your own method on the SalesAutoCreate class, call it from the create method and override it on your custom class, like this.

void  create()
{
    #OCCRetryCount
 
    try
    {
        setprefix("@SYS55110");
 
        ttsbegin;
 
        while (this.recordExist())
        {
 
            this.setCust();
 
            setprefix(#PreFixField(CustTable,AccountNum));
 
            this.setSalesTable();
 
            this.setSalesLine();
 
            setprefix(#PreFixField(SalesLine,ItemId));
 
            //-> TST
            this.deleteProcessed();
            //<- TST
 
            this.nextRecord();
        }
 
        this.endUpdate();
 
        ttscommit;
    }
 
    catch (Exception::Deadlock)
    {
        retry;
    }
 
    catch (Exception::UpdateConflict)
    {
        if (appl.ttsLevel() == 0)
        {
            if (xSession::currentRetryCount() >= #RetryNum)
            {
                throw Exception::UpdateConflictNotRecovered;
            }
            else
            {
                retry;
            }
        }
        else
        {
            throw Exception::UpdateConflict;
        }
    }
 
}
 
//SalesAutoCreate
protected void deleteProcessed()
{
}
 
//TSTSalesImport
protected void deleteProcessed()
{;
    super();
 
    TSTSalesImport.selectForUpdate(true);
    TSTSalesImport.delete();
}

Dynamics Ax Cleaning up the AIF document log

While doing a small AIF project I wrote a small batch class to cleanup the AIF document log because the button on the AifDocumentHistory form can take up a huge amount of time. The first thing I did to write this class is checking out the standard Ax code in the following method ClassesAifMessageManagerclearAllProcessedAndError. This method uses a progress bar and deletes records in batches of 3000 records, this is something we don’t need when running in batch.

The first thing our method needs to do is check if we have access rights to delete.

if (    !hasTableAccess(tablenum(AifMessageLog),    AccessType::Delete)
    ||  !hasTableAccess(tablenum(AifDocumentLog),   AccessType::Delete)
    ||  !hasTableAccess(tablenum(AifCorrelation),   AccessType::Delete))
{
    throw error("@SYS113226");
}

The second step is requesting the permission to skip AOS validation.

skipAOS = new SkipAOSValidationPermission();
skipAOS.assert();

The next step is calling all the skip methods, Microsoft does this to make sure that a delete_from doesn’t fall back to row by row deletes.

  • skipAosValidation : Skips all validation methods (validateWrite, validateDelete, validateField)
  • skipDatabaseLog : Prevents SQL from making transactions logs.
  • skipDataMethods : Forces doInsert, doUpdate instead of insert, update.
  • skipDeleteActions : Skips all actions defined under DeleteActions ( For example: Deleting a SalesTable also deletes all referencing MarkupTrans records. )
  • skipDeleteMethod : Forces doDelete instead of delete.
  • skipEvents : Disables a lot of kernel events to increase performance.
aifMessageLog.skipAosValidation(true);
aifMessageLog.skipDatabaseLog(true);
aifMessageLog.skipDataMethods(true);
aifMessageLog.skipDeleteActions(true);
aifMessageLog.skipDeleteMethod(true);
aifMessageLog.skipEvents(true);
 
aifDocumentLog.skipDatabaseLog(true);
aifDocumentLog.skipDataMethods(true);
aifDocumentLog.skipDeleteActions(true);
aifDocumentLog.skipDeleteMethod(true);
//BP Deviation Documented
aifDocumentLog.skipAosValidation(true);
aifDocumentLog.skipEvents(true);
 
aifCorrelation.skipDatabaseLog(true);
aifCorrelation.skipDataMethods(true);
aifCorrelation.skipDeleteActions(true);
aifCorrelation.skipDeleteMethod(true);
//BP Deviation Documented
aifCorrelation.skipAosValidation(true);
aifCorrelation.skipEvents(true);

After these methods we can start deleting the records, I’ve used a utcDateTimeRemove variable to cleanup records after a certain number of days.

delete_from     aifDocumentLog
    exists join aifMessageLog
        where   aifDocumentLog.MessageId        == aifMessageLog.MessageId
        &&      aifMessageLog.createdDateTime   <= utcDateTimeRemove
        &&     (aifMessageLog.Status            == AifMessageStatus::Processed
                                                ||
                aifMessageLog.Status            == AifMessageStatus::Error);
 
delete_from     aifCorrelation
    exists join aifMessageLog
        where   aifCorrelation.MessageId        == aifMessageLog.MessageId
        &&      aifMessageLog.createdDateTime   <= utcDateTimeRemove
        &&     (aifMessageLog.Status            == AifMessageStatus::Processed
                                                ||
                aifMessageLog.Status            == AifMessageStatus::Error);
 
delete_from aifMessageLog
    where   aifMessageLog.createdDateTime       <= utcDateTimeRemove
    &&     (aifMessageLog.Status                == AifMessageStatus::Processed
                                                ||
            aifMessageLog.Status                == AifMessageStatus::Error);

The final step is to revert the code access permission.

CodeAccessPermission::revertAssert();

Source : msdn xRecord class

(this job should never run on a production environment, build an archiving alternative instead)

(edit if you are using Ax 2012 have a look at this post Dynamics Ax 2012 History cleanup for a better solution)

Dynamics Ax 2009 using the DateTimeUtil

Since i’m getting a lot of google hits on my Dynamics Ax – workdays to days post, i’ve decided to blog some more about it. The DateTimeUtil class is actually a wrapper of the .NET DateTime class.

A first thing to remember when using UtcDateTime EDT’s is that it is stored like the name says as Coordinated Universal Time.

The controls on the form will translate the DateTime to the timezone of the client. Now keeping this in mind is very important when mixing date, time and datetime controls. The following example will make it more clear.

The first field is a UtcDateTimeEdit control with a data method that returns DateTimeUtil::UtcNow().

DateTime debugger

As you can see the time is 08:58, but the first control on the form shows 10:58. This is correct because my client timezone is (GMT+01:00) Brussel, Kopenhagen, Madrid, Parijs and it’s summer time.

The second field is a TimeEdit control with a data method that returns DateTimeUtil::time(DateTimeUtil::utcNow()), this isn’t correct because it will always return the time in the UTC timezone and the control will not translate it to the correct timezone.

The third field is another TimeEdit control with a data method that returns TimeNow(), this is correct because the TimeNow method will also apply the client/server (depending on the tier) timezone.

this also applies to field in a table.

Datetime example
Datetime example

Another way to use Time controls and the DateTimeUtil is using the applyTimeZoneOffset method.

UtcDateTime ret;
;
 
ret = DateTimeUtil::applyTimeZoneOffset(DateTimeUtil::utcNow(),TimeZone::GMTPLUS0100BRUSSELS_COPENHAGEN_MADRID);
 
return  DateTimeUtil::time(ret);

but this is a lot of code for a rather more simple thing 🙂

A second thing to remember, when querying with a date on UtcDateTime fields make sure you select the whole day from 00:00:00 to 23:59:59.

For this I like to implement a method on the Global class, it’s keeps you’re queries cleaner 😉 It works the same way as all .NET developers use, Add a with time 0 and subtract a second.

static UtcDateTime dateCompare(date      _date,
                                  boolean   _toDate = false)
{
    UtcDateTime ret;
    ;
 
    if(_toDate)
    {
        ret = DateTimeUtil::addSeconds(DateTimeUtil::newDateTime(_date + 1,0),-1);
    }
    else
    {
        ret = DateTimeUtil::newDateTime(_date,0);
    }
 
    return ret;
}

So you’re query looks like this.

Date        currectDate = SystemDateGet();
SalesLine   salesLine;
;
 
while select    salesLine
where           salesLine.createdDateTime >= dateCompare(currectDate)
&&              salesLine.createdDateTime  <= dateCompare(currectDate,true)
{
    //Do something
}

Dynamics Ax modifying CreatedDateTime

For testing purposes with the MRP we needed to modify the createdDateTime fields in Dynamics Ax 2009. Since these are system fields we needed a workaround.

if(isRunningOnServer() && CurUserId() == "Admin")
{
            new OverwriteSystemfieldsPermission().assert();
 
            salesLine.overwriteSystemfields(true);
                                                                             // "YYYY/MM/DD HH:MM:SS"
            salesLine.(fieldnum(SalesTable,CreatedDateTime))    = str2datetime( "2010/04/03 11:00:00" ,321 );
            salesLine.doInsert();
            salesLine.overwriteSystemfields(false);
 
            CodeAccessPermission::revertAssert();
}

Remarks:

  • Make sure that it is running on the server tier
  • It only works on insert NOT on update

Since this code is pretty exotic and you don’t want to release this to a production environment we eventually didn’t use this but ran some sql jobs, but this shows that it is possible.

edit
Easy does it 😉

DECLARE @DATAAREAID nvarchar(4)
DECLARE @SALESID    nvarchar(20)
DECLARE @DATETIME   datetime
 
/* EDIT THE FIELDS BELOW */
 
SET     @DATAAREAID     = 'CEU'
SET     @SALESID        = '00003352_058'
SET     @DATETIME       = '01/01/98 00:00:00.000'
 
/* DO NOT EDIT HERE */
 
UPDATE  dbo.SALESTABLE
SET     CREATEDDATETIME = @DATETIME
WHERE   SALESID         = @SALESID
AND     DATAAREAID      = @DATAAREAID;
 
UPDATE  dbo.SALESLINE
SET     CREATEDDATETIME = @DATETIME
WHERE   SALESID         = @SALESID
AND     DATAAREAID      = @DATAAREAID;

Dynamics ax a key with the name %1 already exists

Today I had to fix a bug in some custom code in Dynamics Ax 2009, we had an error from the JournalTransList class that stated “A key with the name %1 already exists.”

  • Cause: A custom field on the WMSJournalTrans table that was extending from LineNum
  • Reason: The JournalTransList has methods to check if the primary index on the actual table won’t be violated, therefore it seems to look for all fields that extend from LineNum.
  • Solution: Create you’re custom EDT that extends from LineNum and add that one to the table, it’s a best practice but who thought this could be so critical :p

Dynamics Ax workdays to days

Ever had to calculate the number of days starting from a number of workdays and even deal with holidays.
A pretty straightforward example using the DateTimeUtil class and DayOfWk method.

static void WorkDaysToDays(Args _args)
{
    int                 workDays    = 10;
    int                 days        = 0;
    date                startDate   = systemDateGet();
    CalendarId          calendarId  = CompanyInfo::find().ShippingCalendarId;
    ;
 
    if(calendarId)
    {
 
        while(workDays > 0)
        {
            if(dayofwk(startDate) != 6 && dayofwk(startdate) != 7 && WorkCalendarDate::isDateOpen(calendarId,startDate))
            {
                workDays--;
                days++;
            }
            else
            {
                days++;
            }
            startDate = DateTimeUtil::date(DateTimeUtil::addDays(DateTimeUtil::newDateTime(startDate,0),1));
        }
        info(strfmt("Workdays = %1, Days = %2",workDays,days));
    }
    else
    {
        error("No calendarId found");
    }
}