Sunday, March 27, 2016

AX 2012 : Generate customer account statement as PDF through X++

Customer account statement in AX 2012 R3 can be generated from Accounts Receivable > Reports > External > Customer account statement. However, if we need to generate this report through code - how can we achieve this?

I created a new query in AOT with only dataSource 'CustTable', as I want to loop on selected customers only. I created a class extending from RunBase (did not want to touch existing report and class for customer account statement) being considering it a custom process of generating customer account statement using X++ code opposed to what exists in system.

New query looks like this with three ranges, these ranges will show on dialog form

















Created a new class with following methods which are common for every dialog class;


  • main
  • construct
  • new
  • pack
  • unpack
  • dialog
  • getFromDialog
  • run
  • showQueryValues
  • queryRun

I will explain important methods from the class which I think need discussion.

QueryRun method - this method will be called from Pack() method from this line of code return [#CurrentVersion,#CurrentList,this.queryRun().pack()];


public QueryRun queryRun()
{
    QueryRun                ret;
    QueryBuildDataSource    qbds;
    CustTable               custTable = CustTable::findRecId(selectedRec);
    if (!queryRun)
    {
        query   =   new query(queryStr(FF_CustAccountStatement));
        qbds = query.dataSourceTable(tableNum(custTable));
        if(selectedRec)
        {
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, AccountNum)).value(custTable.AccountNum);
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, CustGroup)).value(custTable.CustGroup);
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, AccountStatement)).value(SysQuery::value(custTable.AccountStatement));
        }
        else
        {
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, AccountNum)).value(SysQuery::valueUnlimited());
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, CustGroup)).value(SysQuery::valueUnlimited());
            SysQuery::findOrCreateRange(qbds, fieldNum(custTable, AccountStatement)).value(SysQuery::valueUnlimited());
        }
        queryRun    =   new QueryRun(query);
    }
    ret = queryRun;
    return ret;
}

This method generates customer account statement for each provided customer in query. This method is called from run() method;

while (queryRun.next())
    {
        custTable    =   queryRun.get(tableNum(custTable));
        //this.setParameters(custTable);

        filename    =   WinAPI::getTempPath()+"Customer "+ custTable.AccountNum+ " Customer Account Statement.pdf";

        this.createCustAccountStatement(custTable, filename);

    }

public void createCustAccountStatement(CustTable _custTable,FileName _filename)
{
    CustAccountStatementExtController      controller = new CustAccountStatementExtController();
    SRSPrintDestinationSettings            printSettings;
    CustAccountStatementExtContract        Contract;

    controller.parmReportName(PrintMgmtDocType::construct(PrintMgmtDocumentType::CustAccountStatement).getDefaultReportFormat());
    controller.parmLoadFromSysLastValue(true);
    controller.parmShowDialog(false);

    printSettings = controller.parmReportContract().parmPrintSettings();

    printSettings.printMediumType(SRSPrintMediumType::File);
    printSettings.fileFormat(SRSReportFileFormat::PDF);
    printSettings.overwriteFile(true);
    printSettings.fileName(_filename);

    Contract = controller.parmReportContract().parmRdpContract() as CustAccountStatementExtContract;

    enumerator = controller.parmReportContract().parmQueryContracts().getEnumerator();
    enumerator.moveNext();
    query = enumerator.currentValue();
    query.dataSourceTable(tableNum(CustTable)).addRange(fieldNum(CustTable, AccountNum)).value(queryValue(_custTable.AccountNum));

    Contract.parmAgingBucket(agingBucket);
    Contract.parmAgingBucketPrintDescription(AgingBucketPrintDescription);
    Contract.parmAgingPeriod(agingPeriod);
    Contract.parmDayMonth(dayMonth);
    Contract.parmFromDate(fromDate);
    Contract.parmIncludeReversed(includeReversed);
    Contract.parmManualSetup(manualSetup);
    Contract.parmOnlyOpen(onlyOpen);
    Contract.parmPrintAging(printAging);
    Contract.parmPrintAmountGiro(printAmountGiro);
    Contract.parmPrintCreditLimit(printCreditLimit);
    Contract.parmPrintGiro(printGiro);
    Contract.parmPrintingDirection(printingDirection);
    Contract.parmPrintNonZero(printNonZero);
    Contract.parmPrintPaymentSchedule(printPaymentSchedule);
    Contract.parmPrintType(printType);
    Contract.parmSpecifyDueToDate(specifyDueToDate);
    Contract.parmToDate(toDate);
     
    controller.startOperation();
}

It generates report in user's temp folder for each selected customer on below screen [this is custom screen created by a class which I referred above]



AX 2012 - sysOperation Framework implementation [Example]

This post demonstrates how to create batch classes using Business Operation Framework (or sysOperation). We used to use RunBaseBatch framework to implement batch functionality before this new concept of AX services was taken in place.
Compensations of BOF (or sysOperation) framework over RunBaseBatch are very well explained here http://daxmusings.codecrib.com/2011/08/from-runbase-to-sysoperation-business.html

Key points RunBase/RunBaseBatch Vs sysOperation Frameworks
  1. The sysOperation framework is an implementation of the MVC pattern (Model-View-Controller)
  2. Model - parameters [contract class]
  3. View - Dialog [contract class or an optional UI builder class]
  4. Controller - Process (the code that runs in the essence of pattern)
  5. The framework influence the Services framework, whereas a service in AX has a data contract with some special attributes
  6. DataMember attributes in contract class take the entered values in parameters (dialog fields) to the process (aka "operations")
  7. sysOperationFramework simplifies the pack/unpack of variables taking advantage of attributes, this was pretty nasty in RunBaseBatch framework
Let’s jump into example now, we are going to create a batch looks like this;


Create a new Contract Class

General concept of contract class: Contract class in basically an AX class with special attributes as you can see mentioned at the top of this class declaration. This class acts as a model of MVC (model-view-controller) pattern. 
In runbase/runbasebatch framework this model has member variables and pack/unpack (aka serialization). 
These member variables are defined in runbase/runbasebatch class declaration with pack/unpack variables handle under currentVersion and localVersion macros.

Specific to this this example: This class has two attributes; DataContractAttribute which is making it special class to leverage with AX services and the other attribute is referring to UIBuilder class. More on UIBuilder class explains well in this post follows.
[
    DataContractAttribute,
    SysOperationContractProcessingAttribute(classstr(FF_ExampleUIBuilder))
]
class FF_ExampleContract implements SysOperationValidatable
{
    FromDate                fromDate;
    ToDate                  toDate;
    NoYesId                 todaysDate;
    MainAccountNum          mainAccount;
}
[DataMemberAttribute]
public MainAccountNum parmMainAccount(MainAccountNum _mainAccount = mainAccount)
{
    mainAccount = _mainAccount;
    return mainAccount;
}
[DataMemberAttribute]
public FromDate parmFromDate(ExchangeRateImportFromDate _fromDate = fromDate)
{
    fromDate = _fromDate;
    return fromDate;
}
[DataMemberAttribute]
public ToDate parmToDate(ExchangeRateImportToDate _toDate = toDate)
{
    toDate = _toDate;
    return toDate;
}
[DataMemberAttribute]
public NoYesId parmTodaysDate(NoYesId _todaysDate = todaysDate)
{
    todaysDate = _todaysDate;
    return todaysDate;
}
public boolean validate()
{
    boolean ok = true;
    if (mainAccount == "")
    {
        ok = checkFailed("Main account cannot be blank");
    }
    if (todaysDate == NoYes::No)
    {
        if (fromDate == dateNull())
        {
            ok = checkFailed("From date cannot be blank");
        }
        if (toDate == dateNull())
        {
            ok = checkFailed("To date cannot be blank");
        }
    }
    return ok;
}

Create a new UIBuider class;
This class is only required when you want to play with added parameter (data member attributes) in contract class. Like in this example I will be enabling/disabling parameters based on other parameter values, this class can also be used to build custom lookups and perform some custom logic only to play with parameters and their values.

class FF_ExampleUIBuilder extends SysOperationAutomaticUIBuilder
{
    #define.MainAccount('mainAccount')
    #define.TodaysDate('todaysDate')
    #define.FromDate('fromDate')
    #define.ToDate('toDate')
    FormComboBoxControl                 mainAccountControl;
    FormCheckBoxControl                 todaysDateControl;
    FormDateControl                     fromDateControl;
    FormDateControl                     toDateControl;
}
public void build()
{
    DialogGroup                             fromToDateRangeGroup;
    DialogField                             dialogField;
    SAB_RetailConsolidateContract           dataContract;
    dialog = this.dialog();
    dataContract = this.dataContractObject();
    dialogField = dialog.addField(extendedtypestr(MainAccountNum));
    mainAccountControl = dialogField.control();
    mainAccountControl.text(dataContract.parmMainAccount());
    mainAccountControl.name(#MainAccount);
    dialogField = dialog.addField(extendedtypestr(NoYesId));
    todaysDateControl = dialogField.fieldControl();
    todaysDateControl.value(NoYes::No);
    todaysDateControl.name(#TodaysDate);
    todaysDateControl.label("Today");
    fromToDateRangeGroup = dialog.addGroup("Date range");
    fromToDateRangeGroup.columns(2);
    fromToDateRangeGroup.widthMode(FormWidth::ColumnWidth);
    fromToDateRangeGroup.caption('');
    dialogField = dialog.addField(extendedtypestr(FromDate));
    fromDateControl = dialogField.control();
    fromDateControl.name(#FromDate);
    fromDateControl.allowEdit(true);
    fromDateControl.dateValue(systemDateGet());
    fromToDateRangeGroup.addFieldname(#ToDate);
    dialogField = dialog.addField(extendedtypestr(ToDate));
    toDateControl = dialogField.control();
    toDateControl.name(#ToDate);
    toDateControl.allowEdit(true);
    toDateControl.dateValue(systemDateGet());
    fromToDateRangeGroup.addFieldname(#ToDate);
}
/// <summary>
/// Provides form logic related to the date type selected.
/// </summary>
/// <param name="_importDateTypeControl">
/// The <c>FormComboBoxControl</c> control for the date type.
/// </param>
public boolean dateControlSelectionChg(FormCheckBoxControl _importDateTypeControl)
{
    FormDateControl fromDateControlLocal;
    FormDateControl toDateControlLocal;
    boolean ret;
    fromDateControlLocal = this.dialog().dialogForm().runControl(#FromDate);
    toDateControlLocal =  this.dialog().dialogForm().runControl(#ToDate);
    ret = _importDateTypeControl.modified();
    if (_importDateTypeControl.checked() == NoYes::Yes)
    {
        fromDateControlLocal.dateValue(dateNull());
        toDateControlLocal.dateValue(dateNull());
        fromDateControlLocal.allowEdit(false);
        toDateControlLocal.allowEdit(false);
    }
    else
    {
        fromDateControlLocal.dateValue(systemDateGet());
        toDateControlLocal.dateValue(systemDateGet());
        fromDateControlLocal.allowEdit(true);
        toDateControlLocal.allowEdit(true);
    }
    return  ret;
}
/// <summary>
/// Called after the user interface has been built to allow for method overrides.
/// </summary>
public void postRun()
{
    super();
    dialog.dialogForm().formRun().controlMethodOverload(false);
    todaysDateControl.registerOverrideMethod(methodstr(FormCheckBoxControl, modified), methodstr(FF_ExampleUIBuilder, dateControlSelectionChg), this);
}

Create a new Controller class;
This class as a controller class of the MVC pattern and runs the business logic. In this example it is setting dialog title, retrieving dialog values from contract class and from Main method it is calling service class with provided method name and execution mode.

class FF_ExampleController extends SysOperationServiceController
{
    #define.MainAccount('mainAccount')
    #define.TodaysDate('todaysDate')
    #define.ToDate('toDate')
    #define.FromDate('fromDate')
}
public ClassDescription caption()
{
    ClassDescription ret;
    // This is used to identify the batch job within batch processing
    ret = "Example sysOperation Framework";
    return ret;
}
public void getFromDialog()
{
    FormComboBoxControl                 mainAccountControl;
    FormCheckBoxControl                 todaysDateControl;
    FormDateControl                     fromDateControl;
    FormDateControl                     toDateControl;
    DialogForm                          theDialogForm;
    FF_ExampleContract                  FF_ExampleContract;
    theDialogForm = this.dialog().dialogForm();
    super();
    mainAccountControl = theDialogForm.runControl(#MainAccount);
    todaysDateControl = theDialogForm.runControl(#TodaysDate);
    fromDateControl = theDialogForm.runControl(#toDate);
    toDateControl = theDialogForm.runControl(#fromDate);
    FF_ExampleContract = this.getDataContractObject(classStr(FF_ExampleContract));
    if (FF_ExampleContract)
    {
        // Set the values in data contract
        FF_ExampleContract.parmMainAccount(mainAccountControl.text());
        FF_ExampleContract.parmTodaysDate(todaysDateControl.value());
        FF_ExampleContract.parmToDate(fromDateControl.dateValue());
        FF_ExampleContract.parmFromDate(toDateControl.dateValue());
    }
}
public LabelType parmDialogCaption(LabelType _dialogCaption = "")
{
    LabelType caption;
    // This appears as the window name
    caption = "Example for SysOperation Framework";
    return caption;
}
public static FF_ExampleController construct()
{
    return new FF_ExampleController();
}
public static void main(Args args)
{
    FF_ExampleController                        controller;
    identifierName                              className;
    identifierName                              methodName;
    SysOperationExecutionMode                   executionMode;
    [className, methodName, executionMode] = SysOperationServiceController::parseServiceInfo(args);
    controller = new FF_ExampleController(className, methodName, executionMode);
    if (controller.prompt())
    {
        controller.run();
    }
}

Create a new Service class;
This class is an AX service class with [SysEntryPointAttribute] attribute at the top of the method which will be called from action menu item.

class FF_ExampleService
{
    
}
[SysEntryPointAttribute]
public void transactionConsolidation(FF_ExampleContract FF_ExampleContract)
{
    FromDate                            fromDate       = FF_ExampleContract.parmFromDate();
    ToDate                              toDate         = FF_ExampleContract.parmToDate();
    NoYesId                             todaysDate     = FF_ExampleContract.parmTodaysDate();
    MainAccountNum                      mainAccountNum = FF_ExampleContract.parmMainAccount();
    MainAccount                         mainAccount = mainAccount::findByMainAccountId(mainAccountNum);
    if (todaysDate == NoYes::Yes)
    {
        fromDate    = systemDateGet();
        toDate      = systemDateGet();
    }
    /*
    ttsBegin;
    ... call any logic here
    ttsCommit;
    */ 
}

Create a new action menu item; 

Create new action menu item with following properties set as shown;

This menu item is calling controller class with parameters ServiceClass.MethodName with execution mode. If you see the Main method of controller class it is getting these passed parameters in this piece of code and eventually calling transactionConsolidation method of Service class.

[className, methodName, executionMode] = SysOperationServiceController::parseServiceInfo(args);
    controller = new FF_ExampleController(className, methodName, executionMode);

Since BOF services run X++ in the CLR, it is very important to generate incremental CIL after implementing new service in AX.

Another important point; the parameter name in FF_ExampleService.transactionConsolidation method MUST BE same as it is in getFromDialog() method of controller class. Otherwise you will get error object not initialised in getFromDialog method.

FF_ExampleService.transactionConsolidation

[SysEntryPointAttribute]
public void transactionConsolidation(FF_ExampleContract FF_ExampleContract)
{
    
}

FF_ExampleControler

public void getFromDialog()
{
    FormComboBoxControl                 mainAccountControl;
    FormCheckBoxControl                 todaysDateControl;
    FormDateControl                     fromDateControl;
    FormDateControl                     toDateControl;

    DialogForm                          theDialogForm;
    FF_ExampleContract                  FF_ExampleContract;

    theDialogForm = this.dialog().dialogForm();

    super();

    mainAccountControl = theDialogForm.runControl(#MainAccount);
    todaysDateControl = theDialogForm.runControl(#TodaysDate);
    fromDateControl = theDialogForm.runControl(#toDate);
    toDateControl = theDialogForm.runControl(#fromDate);

    FF_ExampleContract = this.getDataContractObject(classStr(FF_ExampleContract));

    if (FF_ExampleContract)
    {
        // Set the values in data contract
        FF_ExampleContract.parmMainAccount(mainAccountControl.text());
        FF_ExampleContract.parmTodaysDate(todaysDateControl.value());
        FF_ExampleContract.parmToDate(fromDateControl.dateValue());
        FF_ExampleContract.parmFromDate(toDateControl.dateValue());
    }
}

How to enable new Microsoft teams - Public Preview!

New Microsoft Teams is just AWESOME, quick but useful post below shows how you have this preview feature to make your life EASY!  Open Micr...