Monday, January 26, 2026

How to effectively detect if data has changed using RecVersion

Sometimes there is a requirement to effectivle detect if data has changed in a table. The first thing that might come to mind is, why not just use the "ModifiedDateTime" field of the data? Well, that's perfectly fine if don't have a lot of data in your table.

But, what if there is a lot of data in our table? Like millions of records? Filtering on an unindexed field like "ModifiedDateTime" and with a lot of data, will force the database server to scan all the records in the table. The database server has to do this to determine which records are modified after the specified date and time. Table scans on large tables is not effective and painfully slow because it's not using any of the table indexes.

RecVersion

So, what's the solution? How do we effectively detect which rows have changed in our data? By using the system field RecVersion. Every table in Dynamics 365 Finance and Operations (F&O) has a field called RecVersion.

Every time a record is changed in a table, the RecVersion of the record is updated by the system to a new value, or version. The RecVersion field contains the record version of the record.

But, there is a small problem, how do know what the previous version of RecVersion was before the data was changed? Well, we don't. But luckily there is a simple way so solve this. By creating a custom table with the RecId and RecVersion of our data.

Steps

  • Create a custom table with a reference RecId and RecVersion field to our data. Field names: RefRecId and RefRecVersion
  • In code, create a method to create a "snapshot" of the RecId and RecVersion values of the data. Use insert_recordset to copy the data.
  • Query your table with a join to the custom table. Join on YourTable.RecId and YourTable.RecVersion and SnapShotTable.RefRecId and SnapShotTable.RefRecVersion

To get all the data from the target table that has changed, join the two tables on RecId and RecVersion. The records where RecVersion is different, has changed.

Sunday, January 25, 2026

Base64 encoding and decoding

What is Base64 encoding and decoding? On a very high level: it is a method of converting binary data to ASCII text and the ASCII text back again to binary. There are many articles online on how this works technically but this article will explain the basics and how Base64 encoding and decoding is used in Dynamics 365 Finance and Operations (F&O).

For example, if you attach a spreadsheet or other binary file like a PDF document to an email. The binary file is converted to ASCII text using Base64 encoding and is embedded in the message body of the email as text. When the email message is received by the recipient, the email application used to view the email message, detects that there is a file attached embedded as text and converts the text back to binary so that the attachment can be downloaded as a file. Most email applications support previewing attachments, but will have to convert the attachments from text to binary on the fly before being able to display the contents of the file.

So, how does Base64 encoding and decoding relate to F&O? It is frequently used in the standard application and also by developers when using the SysOperation framework just to mention just a few examples.

Code example

In a SysOperation contract class, the SysOperation framework allows a Query object to be used to query data for the operation. Because the related operation can also be run in "batch", in other words in "unattended" mode without an user interface, the Query object of the class needs to be saved to the database so that the operation can be executed by the batch framework and use the exact same query object that was specified by the user when adding the operation to batch. In other words, the system saves all query filters, joins and other changes to the query and will then use your exact query when executing in batch.

An instance of an class object cannot be saved to the database. So, how does the system save the object to the database? By using Base64 encoding and decoding. The Query object of the contract class is serialized to a binary object using the "pack" method of the Query class. This method returns a binary representation of the query in a container type. All class variables of the Query object are serialized and converted to a binary object (container). The resulting binary object of the Query object is then Base64 encoded to text. This text can be saved to the database as a regular text field.

When the Query object instance is required by the SysOperation framework, the process is reversed. The text is Base64 decoded to binary, the original Query object with all the specified filters and other changes can then be instantiated by using the binary data to construct a Query object.

1. Class variable for the Base64 encoded text.

2. Code that converts the text back to binary object.

3. Code that converts the binary object to text.

Sunday, January 18, 2026

Using Boolean OR to make where clause fields optional

Sometimes you have a custom "while select" statement and you need to have one or more of the where clause fields in the statement to be optional. In this example, I will use a simple method to display the customer account and group for customers.

The method below displays the customer accounts for customers and uses the _custGroup argument to filter customers that match the customer group. The method works as expected but there is one drawback: it can only show customers for the specified customer group.

So, what if we want to show info for all customers with this method? Can't we just call the method and pass an empty string? The answer is yes, that's possible but will not work as one would expect. The problem with this is, that if you call the method and pass an empty string for the customer group, the while select statement will return all customers where the customer group is empty. Since the customer group field is mandatory, and all customers have a customer group, the query will return nothing.

Show customer info method


//show customer info with mandatory argument
public static void showCustomerInfo(CustGroupId _custGroup)
{
    CustTable custTable;

    while select custTable
        where custTable.CustGroup == _custGroup
    {
        info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
    }

}

How do we solve this problem? Your first thought might be to simply duplicate the while select statement in the method for each argument used in the where clause. In this example two statements, one statement with, and another statement whithout the customer group where clause. And simply add an "if" statement to check if the customer group argument has a value or not. Yes, this will work but this is definitely not the way to go. The reason is because if you have a large while select statement with many lines and a method with multiple arguments. The code will be hard to read and error prone when modifications are done to it.

Optional argument

So, what is the solution? By using the Boolean OR operator in the while select statement. The where clause for customer group is extended with an Boolean OR to contain two parts. The first part of the Boolean OR evaluates the method customer group argument variable, if this part of the Boolean OR evaluates to TRUE, the second part of the OR is simply ignored, effectively ignoring the where clause part for customer group in the second part. However, if the first part of the Boolean OR statement evaluates to FALSE, the second part of the boolean OR is then evaluated which effectively then applies the where clause of the customer group to the statement. Making the where clause optional and having compact and readible code.


//show customer info with optional argument
public static void showCustomerInfo(CustGroupId _custGroupId = '')
{
	CustTable custTable;

	while select custTable
		where (_custGroupId == '' || custTable.CustGroup == _custGroupId)
	{
		info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
	}

}

Complete example

You can run the class using the SysClassRunner class.
e.g. https://[your-environment].operations.eu.dynamics.com/?mi=SysClassRunner&cls=MyTestJob


public final class MyTestJob
{
    public static void main(Args _args)
    {
        CustGroupId custGroup;
        Dialog dlg = new Dialog('Enter customer group');
        DialogField fldCustGroup = dlg.addField(extendedTypeStr(CustGroupId));

        if (dlg.run())
        {
            custGroup = fldCustGroup.value();
            MyTestJob::showCustomerInfo(custGroup);
        }
    }

    //show customer info with optional argument
    public static void showCustomerInfo(CustGroupId _custGroup = '')
    {
        CustTable custTable;

        while select custTable
            where (_custGroup == '' || custTable.CustGroup == _custGroup)
        {
            info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
        }
    }

}

Tuesday, December 30, 2025

The SysOperation framework

The SysOperation framework is used to implement business logic and is widely used in the standard application. It is just one of the many frameworks in the application and it simplifies the creation of tasks that can run in the background.

Like all frameworks in F&O, the SysOperation framework provides rich functionality out of the box, and lets developers focus on coding business logic. Developers do not have to worry about how the business logic is executed, that's all taken care of by the SysOperation framework!

When implementing business logic by using the SysOperation framework, I recommend creating a minimum of three classes. Let's break that down a bit, I'll explain the function of each class.

Data contract class

The data contract class contains the query and parameters required by the operation. When an operation is started interactively, the user is presented with a dialog that allows them to set some parameter values before the operation is executed. After setting the values, the user can either confirm and run the operation or cancel the operation. If the operation is started programmatically, the data contract is populated in code and the dialog is not shown.

Controller class

The controller class manages the execution of the operation. It does not contain any business logic itself and typically provides an entry point (a static main method) that allows the operation to be started from a menu item. The controller also tells the SysOperation framework which method to execute.

Business logic class

The business logic class contains the business logic of the operation. My custom business logic classes always have a single purpose. For example, update the status of data from one value to another.

Basic example

The following basic example simply displays the account number of all customers in the current legal entity. It demonstrates a minimal implementation of the SysOperation framework.

Data contract class


[DataContractAttribute]
public final class MySysOpDemoContract
{
    private boolean showName;
    private str packedQuery;

    [DataMemberAttribute, SysOperationLabel("Show customer name")]
    public boolean parmShowName(boolean _showName = showName)
    {
        showName = _showName;
        return showName;
    }

    public Query getQuery()
    {
        return new Query(SysOperationHelper::base64Decode(packedQuery));
    }

    public void setQuery(Query _query)
    {
        packedQuery = SysOperationHelper::base64Encode(_query.pack());
    }

    [DataMemberAttribute, AifQueryTypeAttribute('_packedQuery', queryStr(CustTable))]
    public str parmQuery(str _packedQuery = packedQuery)
    {
        packedQuery = _packedQuery;
        return packedQuery;
    }
}

As mentioned before, the data contract class contains the query and parameters for the operation. It also includes other variables and methods required by the system to function properly. These methods can be used by the controller class during runtime. Don’t worry too much about the details for now.

[DataContractAttribute] - This attribute is required on the data contract class so that the system knows it should be serialized and deserialized.

private boolean showName - Custom class variable that determines whether the business logic should display the customer name.

private str packedQuery - The system serializes the operation query to a string and stores it in a class variable (required).

[DataMemberAttribute] - Required attribute on the parm method to ensure the showName field is serialized and displayed in the runtime dialog. Without it, the field is excluded from serialization and will not appear in the dialog.

[SysOperationLabel("Show customer name")] - Specifies the label for showName in the runtime dialog. If omitted, the label from the parm method’s extended data type is used.

parmShowName - Runtime parm method for accessing/setting the contract class show name class variable.

getQuery - Returns the query for the contract class at runtime. Required by the system.

setQuery - Sets the query for the contract class at runtime. Required by the system.

parmQuery - Runtime parm property for accessing/setting the contract class query. Required.

Controller class


public class MySysOpDemoController extends SysOperationServiceController
{
    public static void main(Args _args)
    {
        MySysOpDemoController operation = new MySysOpDemoController();
        operation.startOperation();
    }

    public void new(IdentifierName _className = '', IdentifierName _methodName = '', SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Asynchronous)
    {
        super(_className, _methodName, _executionMode);
        
        this.parmClassName(classStr(MySysOpDemoService));
        this.parmMethodName(methodStr(MySysOpDemoService, runQuery));
        this.parmExecutionMode(SysOperationExecutionMode::Synchronous);
        this.parmDialogCaption("My SysOperation demo");
    }
}

The controller class is used to launch the operation interactively from the F&O user interface or from code. Importantly, it provides information to the system about which business logic method to execute.

public static void main(Args _args) - The entry point of the controller class, this method is called by F&O when the operation is launched interactively using a menu item. The method instantiates an instance of our controller class and calls a single method startOperation.

public void new - The new method sets the business logic class name and method name after the call to super. This is how the SysOperation framework knows that our method must be called in the operation. The dialog caption is also set in the new method.

Note: The ExecutionMode in the example is set to Synchronous. That means the operation is executed as soon as the startOperation method is called. More about this later when I update this article.

Business logic class


public class MySysOpDemoService
{
    public void runQuery(MySysOpDemoContract _data)
    {
        Query query = _data.getQuery();
        QueryRun queryRun = new QueryRun(query);

        while (queryRun.next())
        {
            CustTable custTable = queryRun.get(tableNum(CustTable));
            
            if (_data.parmShowName())
            {
                info(strFmt('%1 - %2', custTable.AccountNum, custTable.name()));
            }
            else
            {
                info(custTable.AccountNum);
            }
        }
    }
}

Contains the business logic of the operation. The method must be public and can take only one argument – the data contract. The data contract holds the query and parameters for the operation.

The business logic in the example above does the following:

  • The query is retrieved from the data contract object
  • A QueryRun object is created from the query
  • A while loop enumerates all customers in the query
  • An if statement checks the value of the showName parameter and displays the information accordingly

Note: The data contract class passed as the parameter to the operation method is automatically instantiated by the SysOperation framework and provided to the controller.

Menu item

To launch the example operation from a menu or form in F&O, create a new Action Menu Item, then set its key properties:

  • Label → My SysOperation demo
  • Object → MySysOpDemoController
  • ObjectTypeClass

Once these properties are set, the menu item will open the SysOperation dialog and execute the operation exactly like any standard F&O process.

Basic properties of the menu item.


When the operation is launched via the menu item, the SysOperation framework automatically generates and displays a dialog. Our "Show customer name" parameter appears as a checkbox (slider) control because it is of type boolean. The framework automatically chooses the appropriate dialog control based on the parameter’s data type – for example:

  • str → text box
  • int or real → numeric field
  • boolean → checkbox/slider
  • enum → drop-down list
  • etc.

Output of the operation after clicking OK (with Show customer name disabled).


Launching our operation again and enabling the show customer name parameter.


Output of the operation after clicking OK (with Show customer name enabled).

Note:The output contains the same customer account multiple times, whereas one would expect to see the customer account only once per customer. The reason for this is that we are using the standard CustTable query, which also includes data sources for the CustTrans and CustTransOpen tables. As a result, the query returns open transactions as well. The output therefore displays the customer account once for each open transaction the customer has.

Thursday, November 27, 2025

X++ native types vs objects - the fundamental difference you need to understand

In X++ programming, there are simple variable types and more complex types. Native types are primitive variable types like int, str, real, etc. Simple types can be used to store specific variable values of types such as string, number, integer, and so on.

There are also more advanced types in X++. One of them is classes. A class is a representation of a real-life object, like a customer, vendor, product, bank, person, and so on. It can be anything that requires more than a single value, unlike a simple type. In fact, a class uses simple types to store the data in the object. A class can be instantiated to create an instance or object of that class. The object reference is stored in a variable just like simple types, but there is a fundamental difference between simple types and objects.

The biggest difference between these two types that I want to make clear is that object variables are references to objects. So, two different variables can point to the same object. If you change something in the object using the first variable, those changes will reflect in the second variable, and vice versa. It is key to understand this concept because in X++, this is something that is common in the application.

Code examples

Example 1. Native types

Two native type variables assigned the same value. One is changed but different values are printed to the output.


public static void main(Args _args)
{
    real len1, len2;

    len1 = 1.23;
    len2 = len1;
    len2 = 2;
    
    info(strFmt('len1: %1', len1));
    info(strFmt('len2: %1', len2));
}

// Generated output:
//    len2: 2
//    len1: 1.23

Example 2. Object types

Two variables pointing to the same object, or referencing the same object. I am using tables in this example to make it a bit simpler. Tables are actually classes and you don't have to instantiate them like classes.


public static void main(Args _args)
{
    CustTable custTable1, custTable2;

    custTable1.AccountNum = 'C1001';
    custTable1.CustGroup = 'ABC';
    
    custTable2 = custTable1; // assign object 1 to 2. object 2 is now referencing 1, both are pointing to the object 1.
    
    custTable2.CustGroup = 'XYZ'; // change customer group of object 2, object 1 will also reflect this change because it is pointing to the same object.
    
    info(strFmt('custTable1.AccountNum: %1', custTable1.AccountNum));
    info(strFmt('custTable1.CustGroup: %1', custTable1.CustGroup));
    info(strFmt('custTable2.AccountNum: %1', custTable2.AccountNum));
    info(strFmt('custTable2.CustGroup: %1', custTable2.CustGroup));
}

// Generated output:
//    custTable1.AccountNum: C1001
//    custTable1.CustGroup: XYZ
//    custTable2.AccountNum: C1001
//    custTable2.CustGroup: XYZ

Friday, October 31, 2025

Using a temporary table as a datasource in a form

Dynamics 365 Finance and Operations (F&O) has the ability to utilize temporary tables in code. There are many examples of temporary tables in the standard application. However, sometimes there is a requirement to display data in a temporary table in a form, so that the user can make selections or modify the data in the form before the data is processed.

One example in the standard application is when you post sub ledger documents, e.g. purchase or sales orders. The system presents the user with temporary data before the system posts the data and it becomes final.

Below is a simple example of how to populate a temporary table and display it in a simple form. It simply demonstrates the above-mentioned technique of using a temporary table as a dataSource and does not have any real-world value.

Create a temporary table or use an existing one. Note: The Table Type property of the table must be TempDB. So, if you are using an existing temporary table, make sure the Table Type property is TempDB.

Populate your temporary table with data. Note: The data in your table will be lost when the table buffer goes out of scope.


public class AVTmpTableDataProvider
{
    private AVUpdateDataTmpIdRef tmpIdRef; //added here to keep temp table instance alive while there is an object of this class.

    public AVUpdateDataTmpIdRef populateTmpTableDemo()
    {
        for (int j = 0; j < 100; j++)
        {
            tmpIdRef.Id = j;
            tmpIdRef.Name = guid2Str(newGuid());
            tmpIdRef.insert();
        }

        return tmpIdRef;
    }

}

Create a form and add your temporary table as the dataSource. Set the dataSource property of the grid and add some fields to the grid.

In your form, in the dataSource init method, populate your temporary table and then call the linkPhysicalTableInstance method of the dataSource. Note: You have to call this method after the dataSource has been initialized.


[Form]
public class AVTmpTableDialog extends FormRun
{
    //form class with an instance of our temp table. added here to keep temp table instance alive while the form is open.
    private AVTmpTableDataProvider dataProvider = new AVTmpTableDataProvider();

    [DataSource]
    class AVUpdateDataTmpIdRef
    {
        public void init()
        {
            super();

            //populate temp table in form class and link it to this datasource (after datasource has initialized).
            AVUpdateDataTmpIdRef.linkPhysicalTableInstance(dataProvider.populateTmpTableDemo());
        }

    }
}

Custom service using set based operations

Sometimes you just need a fast way to get specific data from Dynamics 365 Finance (F&O) for real-time processing of F&O data in another application. The data analyst and I were debating the architecture, and exporting the F&O data to a data warehouse was not meeting two of the most important requirements: performance and timeliness (i.e., data that must be up to date and not stale).

The client needs the data from F&O to be up to date and in real time without delays. Since the data output requirement did not involve large datasets, my solution was to use set-based operations to fill a temporary table and to expose the data in a custom service. I am very happy with the performance of this service and definitely recommend using this pattern to everyone out there.

On the highest level, there are basically two methods to insert data into the database: row-based and set-based. Let's briefly discuss both methods so that it's clear why I chose the set-based method.

Set-based operations

With set-based operations, the system creates a single SQL statement and sends it to the SQL Server, even if millions of records are processed in the select part of the statement. This statement selects data from tables and inserts it directly into the target table. It does the selection and insertion at the same time. This is extremely effective, and that makes it so fast.

Microsoft documentation: insert_recordset statement – Copy multiple records directly from one or more tables into another table in one database trip.

Row-based operations

Row-based operations are definitely the most used method in the standard application. They are straightforward and flexible to use, but the drawback is that they're slow. Row-based operations insert each row in a separate database round trip. The system first has to fetch the data, create X++ objects for each row, and then create a separate SQL statement for each X++ object / data row that should be inserted. This whole process adds a lot of overhead and therefore is inherently slow because of its design.

Disclaimer

  • The code below is for demonstration purposes only. Use at your own risk.
  • Don't be a silly Willy and use custom services for large datasets. Use other methods for that.

Okay, enough of all that, let's dive into the code part.

Temporary table

Create a temporary table with the fields that you will need for collecting the data and that will be exposed by the custom service. Set the TableType property to TempDB.


Service class

The code of the custom service class orchestrates data retrieval and prepares the data to be exposed by the custom service framework. When the data is retrieved, it uses a set-based operation, i.e., insert_recordset, which is very fast.

The different steps in the service class are:

  • Retrieve data from tables.
  • Populate the temp table.
  • Enumerate the temp table (for each row in the temp table).
    • Transfer the data from the table to an object.
    • Add the populated object to a list.

The code below is the actual code that I created for the client. It is a real-world working example and has not been simplified, because it would have taken too much time and might contain bugs. I did change the prefixes everywhere in the code below for obvious reasons, and I anonymized some of the dimension field names.

The service has two methods, one to retrieve the balances of ledger accounts for a specified period. The second to retrieve all the transactions for a specified main account. The code below is just the first part because I don't want to make the post longer than needed. The two other helper methods demontrate how to use the set-based operations in X++.


public class AVGenJourAccEntrySvc
{
    [AifCollectionTypeAttribute('return', Types::Class, classStr(AVGenJourAccEntryBalances))]
    public List getBalances(DataAreaId _dataAreaId, AccountingDate _accountingDateFrom, AccountingDate _accountingDateTo)
    {
        AVGenJourAccEntrySvcContract    contract;               //data class for retrieving data.
        AVGenJourAccEntryAggTmp         genJourAccEntryAggTmp;  //temp table where our data is stored.
        AVGenJourAccEntrySvcDP          genJourAccEntrySvcDP;   //logic to retrieve data and populate temp table.
        List                            list = new List(Types::Class); //list of data contracts.

        if (_dataAreaId && _accountingDateFrom && _accountingDateTo)
        {
            contract = new AVGenJourAccEntrySvcContract();

            contract.parmAccountingDateFrom(_accountingDateFrom);
            contract.parmAccountingDateTo(_accountingDateTo);
            contract.parmDataAreaId(_dataAreaId);

            changecompany(contract.parmDataAreaId())
            {
                genJourAccEntrySvcDP = AVGenJourAccEntrySvcDP::construct();
                genJourAccEntryAggTmp = genJourAccEntrySvcDP.populateDataBalances(contract); //retrieve data and populate temp table.

                //get data from temp table
                while select genJourAccEntryAggTmp
                    order by genJourAccEntryAggTmp.MainAccountId,
                             genJourAccEntryAggTmp.MainAccountName,
                             genJourAccEntryAggTmp.Dim1CostCenter,
                             genJourAccEntryAggTmp.Dim2Dept,
                             genJourAccEntryAggTmp.Dim3,
                             genJourAccEntryAggTmp.TransactionCurrencyCode
                {
                    
                    AVGenJourAccEntryBalances balances = AVGenJourAccEntryBalancesenJourAccEntryBalances::construct();
                    balances.initFromAVGenJourAccEntryAggTmp(genJourAccEntryAggTmp); //transfer data from temp table to data contract.

                    list.addEnd(balances); //add data object to list.
                }
            }
        }

        return list;
    }
}

The populateDataBalances method uses two other methods that also use set-based operations. All data collected with this class is done purely with set-based operations, making it extremely fast.


public AVGenJourAccEntryAggTmp populateDataBalances(AVGenJourAccEntrySvcContract _contract)
{
    //1. insert general journal account entry data joining with flattened dimension data.
    this.populateBalanceSheetIncomeStatementMovementsTmp(_contract);

    //2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
    this.populateBalanceSheetClosingsTmp(_contract);

    //3. combine and aggregate result sets.
    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpOpenBal
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpAccDim
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    //4. delete zero amount transactions.
    delete_from genJourAccEntryAggTmp
        where genJourAccEntryAggTmp.AccountingCurrencyAmount == 0;

    return genJourAccEntryAggTmp;
}


//1. insert general journal account entry data joining with flattened dimension data.
private void populateBalanceSheetIncomeStatementMovementsTmp(AVGenJourAccEntrySvcContract _contract)
{
    FiscalCalendarPeriod                        fiscalCalendarPeriod;
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;
    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpOpenBal
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId
                    
        join Dim1CostCenter, Dim2Dept, Dim3
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId
            
        exists join fiscalCalendarPeriod
            where generalJournalEntry.FiscalCalendarPeriod == fiscalCalendarPeriod.RecId &&
            
            generalJournalEntry.Ledger         == Ledger::current()                    &&
            generalJournalEntry.AccountingDate >= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate <= _contract.parmAccountingDateTo()     &&
            fiscalCalendarPeriod.Type          == FiscalPeriodType::Operating          &&
          ((mainAccount.MainAccountId < #FirstIncomeStatmentMainAccount && dimensionCombinationEntity.Dim3 != '') || mainAccount.MainAccountId >= #FirstIncomeStatmentMainAccount); //balance sheet transactions with movements or income statement transactions.
}


//2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
private void populateBalanceSheetClosingsTmp(AVGenJourAccEntrySvcContract _contract)
{
    DimensionValue                              closingMovementCode = '777';
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;

    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpAccDim
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId

        join Dim1CostCenter, Dim2Dept, closingMovementCode
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId &&

            generalJournalEntry.Ledger          == Ledger::current()                    &&
            generalJournalEntry.AccountingDate	>= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate  <= _contract.parmAccountingDateTo()     &&
            mainAccount.MainAccountId           < #FirstIncomeStatmentMainAccount; //only balance sheet transactions.
}

Service

In order to expose your custom service, you need to create a service and specify your service class and methods. Give the service an appropriate name.




Service group

Create a custom service group and add your service class to the group.